From 1d1f63a3e13a095033d9eb6d9dc9236509a6cce8 Mon Sep 17 00:00:00 2001 From: Leo Dion Date: Mon, 18 May 2026 11:48:09 +0100 Subject: [PATCH 01/35] Squashed commit of the following: MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit commit de82483eb71fb9c6ffdfb23ef048f8ce56d4c93c Author: Leo Dion Date: Sun May 17 21:14:35 2026 +0100 git subrepo push Examples/CelestraCloud subrepo: subdir: "Examples/CelestraCloud" merged: "ea897c3" upstream: origin: "git@github.com:brightdigit/CelestraCloud.git" branch: "mistkit" commit: "ea897c3" git-subrepo: version: "0.4.9" origin: "https://github.com/Homebrew/brew" commit: "6f293daa9f" commit 24c87193074cefc18839009f86983f0df6348bb6 Author: Leo Dion Date: Sun May 17 21:14:31 2026 +0100 git subrepo push Examples/BushelCloud subrepo: subdir: "Examples/BushelCloud" merged: "5bb4490" upstream: origin: "git@github.com:brightdigit/BushelCloud.git" branch: "mistkit" commit: "5bb4490" git-subrepo: version: "0.4.9" origin: "https://github.com/Homebrew/brew" commit: "6f293daa9f" commit eee0670d6beea1ff11341d780e084bee64c3a799 Author: Leo Dion Date: Sun May 17 21:14:13 2026 +0100 docs: sync README/CLAUDE examples to v1.0.0-beta.1 API; pin BushelCloud CI; exclude internal Python from CodeFactor - README.md, Examples/BushelCloud/{CLAUDE.md,.docc,.claude/s2s-auth-details.md}, Examples/CelestraCloud/{CLAUDE.md,README.md,.claude/IMPLEMENTATION_NOTES.md}: drop `try CloudKitService(... database: .public)` from init examples (init is non-throwing, `database:` moved per-call); rewrite Quick Start auth around `Credentials` + `APICredentials` / `ServerToServerCredentials` and show `database: .public(.prefers(.serverToServer))` at the call site. - Examples/BushelCloud/.github/workflows/{BushelCloud.yml,bushel-cloud-build.yml}: pin MISTKIT_BRANCH to v1.0.0-beta.1 (matches CelestraCloud) so the subrepo PR builds against the branch that actually carries the new API. Revert to `main` once #298 merges. - .codefactor.yml: exclude Scripts/mermaid-to-pptx.py (internal-use helper). Co-Authored-By: Claude Opus 4.7 (1M context) commit 4d60b1950d7eacf44bf17d90331f4702332e0d33 Author: Leo Dion Date: Sun May 17 20:10:45 2026 +0100 git subrepo push Examples/CelestraCloud subrepo: subdir: "Examples/CelestraCloud" merged: "c44dc4f" upstream: origin: "git@github.com:brightdigit/CelestraCloud.git" branch: "mistkit" commit: "c44dc4f" git-subrepo: version: "0.4.9" origin: "https://github.com/Homebrew/brew" commit: "6f293daa9f" commit 5bc403dc4f2fd30192d59e8d9d0cfe55fb954e06 Author: Leo Dion Date: Sun May 17 20:10:40 2026 +0100 git subrepo push Examples/BushelCloud subrepo: subdir: "Examples/BushelCloud" merged: "55f2092" upstream: origin: "git@github.com:brightdigit/BushelCloud.git" branch: "mistkit" commit: "55f2092" git-subrepo: version: "0.4.9" origin: "https://github.com/Homebrew/brew" commit: "6f293daa9f" commit bce1f235129d31643773e2953404c5b3edf448bb Author: leogdion Date: Sun May 17 20:09:47 2026 +0100 refactor!: prep for talk — shrink API, refactor auth, split OpenAPI (#279) commit 7023a3172a911168ba0d58e9eb3369a00b4454f3 Author: leogdion Date: Fri May 15 12:56:58 2026 -0400 Fixed Nonisolated Web Auth Token (#347) commit f799128296324280fbd7d36e827a165c21fbcd80 Author: leogdion Date: Thu May 14 20:27:28 2026 -0400 Add MistDemo-Integration workflow for live CloudKit runs (#345) commit 418e2e497e84bda8bacf41ab03d46a3ee08a8e04 Author: leogdion Date: Thu May 14 16:03:04 2026 -0400 Resolve #342: v1.0.0-beta.1 follow-ups (#341 #327 #321 #317) + CI fixes (#343) commit d65d20b66db7682d68ac011b7193dc02403e5d49 Author: leogdion Date: Thu May 14 11:25:10 2026 -0400 Resolve #330: interactive MistDemo (web toggle + native app refresh) (#332) commit a28ab3c7c070ec4e655de0d7e129b10308652597 Author: leogdion Date: Mon May 11 16:31:10 2026 -0400 Resolve #313: paginationLimitExceeded carries accumulated records (#326) commit 7a5da7a7bf0796e576ede5f817d3b7c9c7e26e38 Author: leogdion Date: Sat May 9 17:09:53 2026 -0400 Fix CI failures + Claude review nits on PR #298 (v1.0.0-beta.1) (#322) commit b3626c072f053164a5a3f4709821a22ff4fcd4d7 Author: leogdion Date: Sat May 9 16:06:20 2026 -0400 Resolve #312: public+web-auth user-identity endpoints (#310, #311, #27, #28, #34, #35) (#315) * #312 library: add public+web-auth user-identity endpoints and users/caller migration Implements the library side of #312 — adding/renaming user-identity endpoints that require public-database routing with web-auth (user-context) credentials, and unblocking the convenience initializers from their hardcoded database/ environment defaults. #310: `CloudKitService` convenience initializers now accept `database:` and `environment:` parameters with defaults that preserve current behavior. #311: `users/current` → `users/caller`. Renamed in openapi.yaml and the generated client; added a hand-written `fetchCaller()` plus an `@available(*, deprecated, renamed: "fetchCaller")` `fetchCurrentUser()` shim that forwards to the new method. #28: GET `/users/discover` (`discoverAllUserIdentities`). #34: POST `/users/lookup/email` (`lookupUsersByEmail`). #35: POST `/users/lookup/id` (`lookupUsersByRecordName`). The three new endpoints reuse `DiscoverResponse` for parsing — Apple returns `{ users: [UserIdentity] }` for all of them. Each ships with a 5-file test suite mirroring the existing `DiscoverUserIdentities` pattern. #33 (`users/lookup/contacts`) intentionally not implemented: Apple has marked the endpoint deprecated. To be closed as not-planned with a pointer to #34/#35. Co-Authored-By: Claude Opus 4.7 (1M context) * #312 MistDemo: separate database from authentication and add user-context phases Refactors MistDemo's CloudKit configuration model and integration runner to support the public+web-auth combination required by the user-identity endpoints landed in the prior commit. **Configuration refactor.** Replaces the `DatabaseCredentials` enum (which coupled database choice to a single auth method per case, baking in a public⇒S2S/private⇒webAuth assumption) with two orthogonal types: - `AuthenticationCredentials` — `serverToServer(keyID:privateKey:)` / `webAuth(apiToken:webAuthToken:)` - `DatabaseConfiguration` — pairs a `MistKit.Database` with an `AuthenticationCredentials`. The `make(database:authentication:)` factory rejects private+S2S and shared+S2S (which CloudKit rejects) so invalid combinations remain unrepresentable, while public+webAuth is now a valid construction. `MistKitClientFactory.create(for:)` consumes `toPrimaryConfiguration()`; the new `createUserContext(for:)` returns the optional public+web-auth service from `toUserContextConfiguration()` when web-auth tokens are configured. **Phase plumbing.** `PhaseContext` and `IntegrationTestRunner` now thread an optional `userContextService: CloudKitService?`. `PublicDatabaseTest` takes `includeUserContextPhases:` and conditionally appends the new user-identity phases: - `FetchCallerPhase` (renamed from `FetchCurrentUserPhase`) - `DiscoverUserIdentitiesPhase` (existed; updated to use userContextService) - `DiscoverAllUserIdentitiesPhase` (#28) - `LookupUsersByEmailPhase` (#34) - `LookupUsersByRecordNamePhase` (#35) `PrivateDatabaseTest` no longer includes `FetchCurrentUserPhase`: CloudKit rejects `users/caller` against the private database, matching the rest of the user-identity family. **Call-site updates.** `CurrentUserCommand` and `DemoErrorsRunner` swap `fetchCurrentUser()` → `fetchCaller()`. `TestIntegrationCommand` and `TestPrivateCommand` now build and pass `userContextService`. Tests for `AuthenticationCredentials`, `DatabaseConfiguration.make` validation, and `MistDemoConfig.toPrimaryConfiguration` / `toUserContextConfiguration` ship alongside. Co-Authored-By: Claude Opus 4.7 (1M context) * #312: mark discoverAllUserIdentities() unavailable pending #28 investigation Live verification on 2026-05-08 against iCloud.com.brightdigit.MistDemo returned HTTP 500 from Apple's GET /users/discover. The first 12 phases of mistdemo test-integration --verbose run green (the 8 base public+S2S phases plus FetchCallerPhase, DiscoverUserIdentitiesPhase, LookupUsersByEmailPhase, LookupUsersByRecordNamePhase) — only discoverAllUserIdentities fails, blocking phases beyond it. The endpoint is referenced in CloudKitJS but does not appear in Apple's CloudKit Web Services REST documentation. The actual REST shape is still under investigation under #28. Changes: - Marked `CloudKitService.discoverAllUserIdentities()` `@available(*, unavailable, message: ...)` with a pointer to #28. - Removed `DiscoverAllUserIdentitiesPhase` from MistDemo and from `PublicDatabaseTest.phases`. - Removed the `CloudKitServiceDiscoverAllUserIdentities` test directory (the unavailable method cannot be called from Swift code). The OpenAPI definition, generated client, path builder, response processor, Output extension, and Swift wrapper are all retained. Unblocking is a one-line `@available` removal once the correct REST shape is determined under #28. Co-Authored-By: Claude Opus 4.7 (1M context) * #315: resolve PR review — Credentials API, per-call database, cascade unavailable Addresses all four review threads on PR #315: - Comment #1 (error wording): removed `unsupportedDatabaseAuthCombination` along with `MistDemo.DatabaseConfiguration`; invalid combos now surface as `CloudKitError.missingCredentials` from the library. - Comment #2 (per-call database): user-identity ops in `CloudKitService+UserOperations` hardcode `.public`; record/zone/asset/sync ops accept `database: Database? = nil` falling back to a service-level default. - Comment #3 (unified credentials): new `Credentials` / `ServerToServerCredentials` / `APICredentials` value types replace the legacy `apiToken:`/`webAuthToken:` initializers. The token manager is selected based on the target database (S2S for `.public`, web-auth for `.private`/`.shared`). Lifted `PrivateKeyMaterial` into the library. - Comment #4 (cascade unavailable): removed `Operations.discoverAllUserIdentities.Output: CloudKitResponseType` conformance entirely; `processDiscoverAllUserIdentitiesResponse` is now `@available(*, unavailable)` with a `fatalError` body. Also migrates ~15 MistKit test helpers and the MistDemo factory to the new Credentials API. Breaking changes (pre-1.0): removed legacy `CloudKitService` initializers taking `apiToken:`/`webAuthToken:`; `CloudKitService.apiToken` is removed, `.database` is now `internal`. Out of scope: per-call `TokenManager` dispatch (would let one service mix S2S-for-public and web-auth-for-user-context). MistDemo still constructs a separate `userContextService` for that scenario. Co-Authored-By: Claude Opus 4.7 (1M context) * #315: drop service-level database, per-call credential resolution [skip ci] Resolves the architectural feedback in the PR-315 review: * CloudKitService no longer carries `database` — operations take `database:` per call (defaulting to `.public`); user-identity routes drop the parameter since CloudKit pins them to `.public`. Subsumes Claude's "fetchCaller bypasses self.database" finding. * Credentials.makeTokenManager(for:requiresUserContext:) resolves the appropriate token manager at dispatch time. A single service can now serve public-database S2S record ops and user-identity web-auth routes from one fully-populated `Credentials`. MistKitClient.swift is obsolete and removed; per-call dispatch lives in CloudKitService+ClientDispatch. * Credentials.swift split per SwiftLint one_file_per_declaration into ServerToServerCredentials.swift + APICredentials.swift + Credentials.swift. New typed CredentialsValidationError; init asserts in debug, throws in release (no more precondition crash for dynamic config). * MistDemo: userContextService workaround collapsed — single service handles all phases via per-call resolution. * CI hotfix: 11 unused `public import` lines demoted to `internal` (the warnings-as-errors regression flagged in the review). * Tests: 12-case routing-matrix unit suite for makeTokenManager and a fetchCaller suite parallel to LookupUsers* (success + validation). Obsolete MistKitClient tests removed. * Polish: shorter @available message on discoverAllUserIdentities, structural comment for GET /users/discover in openapi.yaml, ConfigurationError.missingAPIToken (unused) removed. 475/475 tests pass. Library + MistDemo build clean. Co-Authored-By: Claude Opus 4.7 (1M context) * Per review on PR #315: listZones, lookupZones, fetchZoneChanges now default to .private since the public database only contains _defaultZone, making .public a degenerate default. MistDemo callers pass context.database / config.base.database explicitly so the --database flag still drives the test runs. Also repairs MistDemo test breakage from 7debe8d: toUserContextCredentials() was removed but tests still referenced it; rewritten against the replacement surface (toPrimaryCredentials embeds apiAuth on .public, plus the new hasUserContextCredentials boolean). The CredentialsValidationTests suite was deleted — it asserted init-time validation that no longer exists under per-call credential resolution; the equivalent .missingCredentials behavior is covered in MistKitTests. Co-Authored-By: Claude Opus 4.7 (1M context) * #312: gate @available(*,unavailable) on processDiscoverAllUserIdentitiesResponse to Swift 6.2+ Swift 6.1 rejects calls to an unavailable function from within another unavailable function; 6.2 relaxed that rule. The internal helper processDiscoverAllUserIdentitiesResponse is unavailable in lockstep with its only caller — the also-unavailable CloudKitService.discoverAllUserIdentities() — which built fine on 6.2+ but failed on Swift 6.1 with: error: 'processDiscoverAllUserIdentitiesResponse' is unavailable: Pending #28: discoverAllUserIdentities is not yet ready. Wrap just the attribute in `#if swift(>=6.2)` so the body is shared and 6.1 compiles. Inline doc records the intent and the one-line cleanup (delete the #if/#endif) once 6.1 is dropped from the matrix. A `swiftlint:disable:next unavailable_function` is required because swiftlint does not evaluate #if and otherwise sees a fatalError-only function without the attribute. Verified: swift build + swift test pass on Swift 6.1.3 (Linux container) and on macOS Swift 6.2+ (475/475 tests). Co-Authored-By: Claude Opus 4.7 (1M context) * #315: split unhandled-response logging into debug (full body) + warning (type/status only) CodeQL's swift/cleartext-logging flagged the existing warning logs because lookupUsersByEmail(_:) propagates email-PII taint through the response object. Move full \(response) interpolation to .debug so the detail stays available for development without flowing into ops logs; keep .warning at type(of:) + HTTP status code only. Co-Authored-By: Claude Opus 4.7 (1M context) * #312: add --lookup-email / CLOUDKIT_LOOKUP_EMAIL to exercise users/lookup/email LookupUsersByEmailPhase previously skipped whenever fetchCaller() didn't return an email (which is the common case). Plumb a configurable lookup email through TestIntegrationConfig / TestPrivateConfig → PhaseContext so the phase can be driven against a known-discoverable iCloud account. Falls back to caller email, then to a clearer skip message naming the flag/env var. Co-Authored-By: Claude Opus 4.7 (1M context) * docs: point CLAUDE.md lint section at mise (and Scripts/lint.sh) swift-format / swiftlint / periphery are pinned in mise.toml; the previous "requires swiftlint installation" wording led to PATH lookups that fail in this repo. Replace with `mise exec --` invocations and flag the full ./Scripts/lint.sh pipeline. Co-Authored-By: Claude Opus 4.7 (1M context) * #315: address review punch list — invalidPrivateKey, recoverable unavailable response, supportsUserContextPhases derivation - CloudKitError: add invalidPrivateKey(path:underlying:) so PEM-load failures carry the file path + original error instead of bare Foundation NSError. Wrap loadPEM() at the single call site in Credentials+TokenManager. Add PrivateKeyMaterial.filePath accessor for the diagnostic. - processDiscoverAllUserIdentitiesResponse: replace fatalError with throw CloudKitError.unsupportedOperationType so a stray Swift 6.1 caller (where the @available cascade does not apply) gets a recoverable error instead of a crash. - TestPrivateCommand: derive supportsUserContextPhases from config.base.hasUserContextCredentials, mirroring TestIntegrationCommand, so user-identity phases skip cleanly when web-auth env vars are absent. - toPrimaryCredentials: replace try? with do/catch + stderr INFO line so operators see when web-auth is missing on a .public setup. - CLAUDE.md: annotate discoverAllUserIdentities() as unavailable pending #28. - CredentialsTokenManagerTests: fill the missing routing-matrix branches (user-context × .private/.shared, .shared + token-only) and cover the new .invalidPrivateKey path. Co-Authored-By: Claude Opus 4.7 (1M context) --------- Co-authored-by: Claude Opus 4.7 (1M context) commit 6f92a7190469244b0b9fd667dadb17f8d90add05 Author: leogdion Date: Fri May 8 13:16:56 2026 -0400 Resolve #308: docs refresh + CI fixes + sub-issues #165, #285 (#309) commit a1e2162a5a827f699c81634de3232dc9bff511a5 Author: leogdion Date: Fri May 8 07:16:10 2026 -0400 Add query pagination support with continuation markers (#306) commit c62bf44a772fc7d70f277bfd6bc1093966e29907 Author: leogdion Date: Thu May 7 15:52:45 2026 -0400 Improve error handling: typed TokenManagerError and safe RecordOperation conversion (#305) commit 7c4b678bfd5baee601e7736ab5e971b234804e29 Author: leogdion Date: Thu May 7 11:27:10 2026 -0400 git subrepo push Examples/CelestraCloud subrepo: subdir: "Examples/CelestraCloud" merged: "4244497" upstream: origin: "git@github.com:brightdigit/CelestraCloud.git" branch: "mistkit" commit: "4244497" git-subrepo: version: "0.4.9" origin: "https://github.com/Homebrew/brew" commit: "b9763ee528" commit f14e751d86f163cecf66fbfbc85cc0c7b501568f Author: leogdion Date: Thu May 7 11:27:07 2026 -0400 git subrepo push Examples/BushelCloud subrepo: subdir: "Examples/BushelCloud" merged: "123a732" upstream: origin: "git@github.com:brightdigit/BushelCloud.git" branch: "mistkit" commit: "123a732" git-subrepo: version: "0.4.9" origin: "https://github.com/Homebrew/brew" commit: "b9763ee528" commit a0f0af93c551c9e4fede8b52942fb3bfe7e171e7 Author: leogdion Date: Thu May 7 11:26:32 2026 -0400 updating example packages commit 125dab50f2fecbc721319b03ba4b256fd543e705 Author: leogdion Date: Thu May 7 11:01:18 2026 -0400 Refactor AuthenticationMiddleware so each Authenticator applies itself (#294) commit f989fd14ad56844b1381550ee604167c075ed739 Author: leogdion Date: Thu May 7 10:23:23 2026 -0400 Strengthen environment and database configuration validation (#293) commit b0f00a770905f1e36110940a8f26129c94658c96 Author: leogdion Date: Thu May 7 10:18:52 2026 -0400 Add operation classification and batch sync result tracking (#296) commit 63a4e503b9833e0e301734c0b27d8875f418c8df Author: leogdion Date: Thu May 7 10:09:27 2026 -0400 Move CloudKitResponseType default implementations to protocol extension (#292) commit ae1af156ce3afd8c5861cd90c6ce580222917948 Author: leogdion Date: Wed May 6 20:20:44 2026 -0400 Test suite improvements for v1.0.0-beta.1 (#286) (#287) commit 5475bfac896cfa603b524c870bfc902fd86d1200 Author: leogdion Date: Tue May 5 20:21:32 2026 -0400 MistDemo: --database flag + demo-errors command (closes #259, #269) (#282) commit 8b21425fd43b5c66f0432f63215b66546e3960a5 Author: leogdion Date: Tue May 5 20:21:17 2026 -0400 Refactor IntegrationTestRunner into protocol-based phase pipeline (#254) (#283) commit 9709f3d89f611e70d09773497fb7f755f0cd6c15 Author: leogdion Date: Tue May 5 08:54:16 2026 -0400 Replace custom AsyncChannel with swift-async-algorithms (#280) commit d53467a5ff7fb2a222ffe301fd5925f24a6bcbc9 Author: leogdion Date: Mon May 4 12:49:25 2026 -0400 CI Updates for May 2026 (#277) commit d7b1a21a0efbf68d9ede1919ac7f7b42b0cdf1e5 Author: Leo Dion Date: Thu Apr 30 09:39:09 2026 -0400 MistDemo improvements: test split, CRUD, auth fix, native app (#271) (#273) commit 0ab2ab60f2d1297fc3d356c4969f1673cb1b2081 Author: leogdion Date: Wed Apr 29 15:49:34 2026 -0400 First Draft Revision of Docs (#268) --- docs/cloudkit-guide/README.md | 4 + ...uthenticating-cloudkit-backend-services.md | 6 +- .../articles/deploying-mistkit-server-side.md | 8 +- .../rebuilding-mistkit-claude-code-part-1.md | 535 ++++ .../rebuilding-mistkit-claude-code-part-2.md | 195 ++ docs/talk-feedback.md | 63 + docs/transcriptions/paragraphs.json | 1 + docs/transcriptions/timestamps.json | 1 + docs/transcriptions/transcript.srt | 2708 +++++++++++++++++ docs/transcriptions/transcript.txt | 177 ++ docs/transcriptions/transcript.vtt | 2023 ++++++++++++ 11 files changed, 5717 insertions(+), 4 deletions(-) create mode 100644 docs/cloudkit-guide/articles/rebuilding-mistkit-claude-code-part-1.md create mode 100644 docs/cloudkit-guide/articles/rebuilding-mistkit-claude-code-part-2.md create mode 100644 docs/talk-feedback.md create mode 100644 docs/transcriptions/paragraphs.json create mode 100644 docs/transcriptions/timestamps.json create mode 100644 docs/transcriptions/transcript.srt create mode 100644 docs/transcriptions/transcript.txt create mode 100644 docs/transcriptions/transcript.vtt diff --git a/docs/cloudkit-guide/README.md b/docs/cloudkit-guide/README.md index eb17d0c9..3f81f447 100644 --- a/docs/cloudkit-guide/README.md +++ b/docs/cloudkit-guide/README.md @@ -238,6 +238,10 @@ Each error carries nested JSON: `ckErrorCode`, `serverRecord` (on 409), `reason` ##### Integrating MistKit +###### Web Application + +###### Background Job + **Three-Layer Architecture**: **Problem**: OpenAPI-generated code is verbose and low-level. diff --git a/docs/cloudkit-guide/articles/authenticating-cloudkit-backend-services.md b/docs/cloudkit-guide/articles/authenticating-cloudkit-backend-services.md index 7847959c..f1f59673 100644 --- a/docs/cloudkit-guide/articles/authenticating-cloudkit-backend-services.md +++ b/docs/cloudkit-guide/articles/authenticating-cloudkit-backend-services.md @@ -2,6 +2,8 @@ title: Beyond the MistKit Tutorials: Authenticating CloudKit from Backend Services date: 2026-01-01 00:00 description: A practical walkthrough of the three CloudKit Web Services authentication methods — API tokens, web auth tokens, and server-to-server signing — and how to wire them up from a backend Swift service using MistKit. +featuredImage: /media/tutorials/[VERIFY: path to hero image] +subscriptionCTA: Subscribe for more deep dives on running Swift on the server. --- @@ -14,8 +16,8 @@ This article is the guide I wish I'd had: a practical walkthrough of the three a **In this series:** -* [Rebuilding MistKit with Claude Code (Part 1)](https://brightdigit.com/tutorials/rebuilding-mistkit-claude-code-part-1/) -* [Rebuilding MistKit with Claude Code (Part 2)](https://brightdigit.com/tutorials/rebuilding-mistkit-claude-code-part-2/) +* [Rebuilding MistKit with Claude Code (Part 1)](/tutorials/rebuilding-mistkit-claude-code-part-1/) +* [Rebuilding MistKit with Claude Code (Part 2)](/tutorials/rebuilding-mistkit-claude-code-part-2/) * _Beyond the MistKit Tutorials: Authenticating CloudKit from Backend Services_ --- diff --git a/docs/cloudkit-guide/articles/deploying-mistkit-server-side.md b/docs/cloudkit-guide/articles/deploying-mistkit-server-side.md index d52212d6..cb316a2a 100644 --- a/docs/cloudkit-guide/articles/deploying-mistkit-server-side.md +++ b/docs/cloudkit-guide/articles/deploying-mistkit-server-side.md @@ -2,10 +2,14 @@ title: Deploying MistKit - From Local CLI to a Scheduled CloudKit Job in CI date: 2026-06-01 00:00 description: A practical walkthrough of running a MistKit-based service or scheduled job in production - how to build a static Linux binary, manage CloudKit credentials, and structure GitHub Actions workflows for tiered scheduled sync. Built around two real production deployments, BushelCloud and CelestraCloud. +featuredImage: /media/tutorials/[VERIFY: path to hero image] +subscriptionCTA: Subscribe for more deep dives on running Swift on the server. --- + + The hard part of using MistKit on a backend isn't writing the code - it's deciding where the code runs, how the credentials get there, and what happens when nobody's watching. Once you've got CloudKit working from a local CLI, the next question is: how do I run this on a schedule, on Linux, without a Mac in the loop? This article is the deployment guide that picks up where the [authentication walkthrough](/tutorials/authenticating-cloudkit-backend-services/) leaves off. Instead of focusing on which auth method to pick, it focuses on the operational side: how to build, package, and run a MistKit-based service so it works reliably on a server, in a container, or as a scheduled CI job. Two production deployments - [BushelCloud](https://github.com/brightdigit/BushelCloud) and [CelestraCloud](https://github.com/brightdigit/CelestraCloud) - are used throughout as worked examples, because both ship today as scheduled CI jobs that write to a CloudKit public database from stock Ubuntu runners. @@ -14,8 +18,8 @@ This article is the deployment guide that picks up where the [authentication wal **In this series:** -* [Rebuilding MistKit with Claude Code (Part 1)](https://brightdigit.com/tutorials/rebuilding-mistkit-claude-code-part-1/) -* [Rebuilding MistKit with Claude Code (Part 2)](https://brightdigit.com/tutorials/rebuilding-mistkit-claude-code-part-2/) +* [Rebuilding MistKit with Claude Code (Part 1)](/tutorials/rebuilding-mistkit-claude-code-part-1/) +* [Rebuilding MistKit with Claude Code (Part 2)](/tutorials/rebuilding-mistkit-claude-code-part-2/) * [Authenticating CloudKit from Backend Services](/tutorials/authenticating-cloudkit-backend-services/) * _Deploying MistKit: From Local CLI to a Scheduled CloudKit Job in CI_ diff --git a/docs/cloudkit-guide/articles/rebuilding-mistkit-claude-code-part-1.md b/docs/cloudkit-guide/articles/rebuilding-mistkit-claude-code-part-1.md new file mode 100644 index 00000000..03cb6b26 --- /dev/null +++ b/docs/cloudkit-guide/articles/rebuilding-mistkit-claude-code-part-1.md @@ -0,0 +1,535 @@ +--- +title: Rebuilding MistKit with Claude Code - From CloudKit Docs to Type-Safe Swift (Part 1) +date: 2025-12-01 00:00 +description: Follow the journey of rebuilding MistKit using Claude Code and swift-openapi-generator. Learn how OpenAPI specifications transformed Apple's CloudKit documentation into a type-safe Swift client, and discover the challenges of mapping CloudKit's quirky REST API to modern Swift patterns. +featuredImage: /media/tutorials/rebuilding-mistkit-claude-code/mistkit-rebuild-part1-hero.webp +subscriptionCTA: Want to learn more about AI-assisted Swift development? Sign up for our newsletter to get notified when Part 2 drops. +--- + +In my previous article about [Building SyntaxKit with AI](https://brightdigit.com/tutorials/syntaxkit-swift-code-generation/), I explored how with the help of [Claude Code](https://claude.ai/claude-code) I could transform SwiftSyntax's 80+ lines of verbose API calls into 10 lines of elegant, declarative Swift. + +I saw how Claude Code could easily replace and understand patterns. That's when I decided to explore the idea of updating [MistKit](https://github.com/brightdigit/MistKit), my library for server-side CloudKit application and see how Claude Code can help. + +--- + +**In this series:** + +* [Building SyntaxKit with AI](/tutorials/syntaxkit-swift-code-generation/) +* _Rebuilding MistKit with Claude Code (Part 1)_ +* [Rebuilding MistKit with Claude Code (Part 2)](/tutorials/rebuilding-mistkit-claude-code-part-2/) + +--- + +📚 **[View Documentation](https://swiftpackageindex.com/brightdigit/MistKit/documentation)** | 🐙 **[GitHub Repository](https://github.com/brightdigit/MistKit)** + +- [The Decision to Rebuild](#the-decision-to-rebuild) + - [The Game Changer: swift-openapi-generator](#the-game-changer-swift-openapi-generator) + - [Learning from SyntaxKit's Pattern](#learning-from-syntaxkits-pattern) +- [Building with Claude Code](#building-with-claude-code) + - [Why OpenAPI + swift-openapi-generator?](#why-openapi--swift-openapi-generator) + - [Challenge #1: Type System Polymorphism](#challenge-1-type-system-polymorphism) + - [Challenge #2: Authentication Complexity](#challenge-2-authentication-complexity) + - [Challenge #3: Error Handling](#challenge-3-error-handling) + - [Challenge #4: API Ergonomics](#challenge-4-api-ergonomics) + - [The Iterative Workflow with Claude](#the-iterative-workflow-with-claude) +- [What's Next](#whats-next) + + +## The Decision to Rebuild + +I had a couple of use cases where MistKit running in the cloud would allow me to store data in a public database. However I hadn't touched the library in a while. + +By now, [Swift had transformed](https://brightdigit.com/tutorials/swift-6-async-await-actors-fixes/) while MistKit stood still: +- **Swift 6** with strict concurrency checking +- **async/await** as standard (not experimental) +- **Server-side Swift maturity** (Vapor 4, swift-nio, AWS Lambda) +- **Modern patterns** expected (Result types, AsyncSequence, property wrappers) + +MistKit, frozen in 2021, couldn't take advantage of any of this. + +> youtube https://youtu.be/_-k97s1ZPzE + + +### The Game Changer: [swift-openapi-generator](https://github.com/apple/swift-openapi-generator) + +At [WWDC 2023](https://developer.apple.com/videos/play/wwdc2023/10171/), Apple announced [`swift-openapi-generator`](https://github.com/apple/swift-openapi-generator)—a tool that reads OpenAPI specifications and automatically generates type-safe Swift client code. This single tool made the MistKit rebuild feasible. What was missing was an OpenAPI spec. If I had that I could easily create a library which made the necessary calls to CloudKit as needed, as well as compatibility with [server-side (AsyncHTTPClient)](https://github.com/swift-server/swift-openapi-async-http-client) or [client-side (URLSession)](https://github.com/apple/swift-openapi-urlsession) APIs . + +That's where [Claude Code](https://claude.ai/claude-code) came in. + + +### Learning from SyntaxKit's Pattern + +With my work on SyntaxKit, I could see that if I fed sufficient documentation on an API to an LLM, it can understand how to develop against it. There may be issues along the way. However, any failures come with the ability to learn and adapt either with internal documentation or writing sufficient tests. + +Just as I was able to simplify SwiftSyntax into a simpler API with [SyntaxKit](https://github.com/brightdigit/SyntaxKit), I can have an LLM create an OpenAPI spec for CloudKit. + +--- + +The pattern was clear: **give Claude the right context, and it could translate Apple's documentation into a usable OpenAPI spec**. SyntaxKit taught me that code generation works best when you have a clear source of truth—for SyntaxKit it was SwiftSyntax ASTs, for MistKit it would be CloudKit's REST API documentation. The abstraction layer would come later. + +The rebuild was ready to begin. + +![CloudKit Web Services Documentation Site](/media/tutorials/rebuilding-mistkit-claude-code/cloudkit-documentation.webp) + + +## Building with [Claude Code](https://claude.ai/claude-code) + +I needed a way for Claude Code to understand how the CloudKit REST API worked. There was one main document I used—the [CloudKit Web Services Documentation Site](https://developer.apple.com/library/archive/documentation/DataManagement/Conceptual/CloudKitWebServicesReference/). The [CloudKit Web Services Documentation](https://developer.apple.com/library/archive/documentation/DataManagement/Conceptual/CloudKitWebServicesReference/) Site, **which hasn't been updated since June of 2016**, contains the most thorough documentation on how the REST API works and hopefully can provide enough for Claude to start crafting the OpenAPI spec. + +By running the site (as well as the swift-openapi-generator documentation) through llm.codes, saving the exported markdown documentation in the `.claude/docs` directory and letting Claude Code know about it (i.e. add a reference to it in Claude.md), I could now start having Claude Code translate the documentation into a usable API. + +### Setting Up Claude Code for MistKit + +Before diving in, here's what you need to understand about working with Claude Code: + +**Documentation Export with llm.codes** +I used [llm.codes](https://llm.codes) (mentioned in my [SyntaxKit article](https://brightdigit.com/tutorials/syntaxkit-swift-code-generation/)) to convert Apple's web documentation into markdown format that Claude can easily understand. This tool crawls documentation sites and exports them as clean markdown files. It also works with DocC documentation from Swift packages, making it easy to give Claude context about any Swift library's API. + +**Claude Code's Context System** +Claude Code uses a simple but powerful context system: +- `.claude/docs/` - Store reference documentation (like CloudKit API docs, swift-openapi-generator guides) +- `.claude/CLAUDE.md` or `CLAUDE.md` - Reference these docs so Claude knows to use them as context + +This gives Claude the context it needs to understand CloudKit's API without you having to paste documentation repeatedly in every conversation. + +``` +.claude/docs +├── cktool-full.md # Complete CloudKit CLI tool documentation +├── cktool.md # Condensed CloudKit CLI reference +├── cktooljs-full.md # Full CloudKitJS documentation +├── cktooljs.md # CloudKitJS quick reference +├── cloudkit-public-database-architecture.md +├── cloudkit-schema-plan.md +├── cloudkit-schema-reference.md +├── cloudkitjs.md # JavaScript SDK documentation +├── data-sources-api-research.md +├── firmware-wiki.md +├── https_-swiftpackageindex.com-apple-swift-log-main-documentation-logging.md +├── https_-swiftpackageindex.com-apple-swift-openapi-generator-1.10.3-documentation-swift-openapi-generator.md +├── https_-swiftpackageindex.com-brightdigit-SyndiKit-0.6.1-documentation-syndikit.md +├── mobileasset-wiki.md +├── protocol-extraction-continuation.md +├── QUICK_REFERENCE.md +├── README.md +├── schema-design-workflow.md +├── sosumi-cloudkit-schema-source.md +├── SUMMARY.md +├── testing-enablinganddisabling.md +└── webservices.md # Primary CloudKit Web Services REST API documentation +``` + +Note: Files with "-full" suffix contain complete documentation exported from llm.codes, while shorter versions are condensed for quicker reference. The swift-openapi-generator docs were essential for understanding type overrides and middleware configuration. + + +### Why OpenAPI + [swift-openapi-generator](https://github.com/apple/swift-openapi-generator)? + +With [`swift-openapi-generator`](https://github.com/apple/swift-openapi-generator) available (announced WWDC 2023), the path forward became clear: + +1. **Create OpenAPI specification from CloudKit documentation** + - Translate Apple's prose docs → Machine-readable YAML + - Every endpoint, parameter, response type formally defined + +2. **Let swift-openapi-generator generate the client** + - Run `swift build` → 10,476 lines of type-safe networking code appear + - Request/response types (Codable structs) + - API client methods (async/await) + - Type-safe enums, JSON handling, URL building + - Configuration: `openapi-generator-config.yaml` + Swift Package Manager build plugin + +3. **Build clean abstraction layer on top** + - Wrap generated code in friendly, idiomatic Swift API + - Add TokenManager for authentication + - CustomFieldValue for CloudKit's polymorphic types + +By following [spec-driven development](https://brightdigit.com/tutorials/swift-openapi-generator/), we had many benefits: + +- Type safety (if it compiles, it's valid CloudKit usage) +- Completeness (every endpoint defined) +- Maintainability (spec changes = regenerate code) +- No manual JSON parsing or networking boilerplate +- Cross-platform (macOS, iOS, Linux, server-side Swift) + + +### Challenge #1: Type System Polymorphism +[CloudKit fields](https://developer.apple.com/library/archive/documentation/DataManagement/Conceptual/CloudKitWebServicesReference/Types.html#//apple_ref/doc/uid/TP40015240-CH3-SW2) are dynamically typed—one field can be STRING, INT64, DOUBLE, TIMESTAMP, BYTES, REFERENCE, ASSET, LOCATION, or LIST. But [OpenAPI is statically typed](https://spec.openapis.org/oas/latest.html). How do we model this polymorphism? + +```no-highlight +Me: "Here's CloudKit's field value structure from Apple's docs. + A field can have value of type STRING, INT64, DOUBLE, TIMESTAMP, + BYTES, REFERENCE, ASSET, LOCATION, LIST..." + +Claude: "This is a discriminated union. Try modeling with oneOf in OpenAPI: + The value property can be oneOf the different types, + and the type field acts as a discriminator." + +Me: "Good start, but there's a CloudKit quirk: ASSETID is different + from ASSET. ASSET has full metadata, ASSETID is just a reference." + +Claude: "Interesting! You'll need a type override in the generator config: + typeOverrides: + schemas: + FieldValue: CustomFieldValue + Then implement CustomFieldValue to handle ASSETID specially." + +Me: "Perfect. Can you generate test cases for all field types?" + +Claude: "Here are test cases for STRING, INT64, DOUBLE, TIMESTAMP, + BYTES, REFERENCE, ASSET, ASSETID, LOCATION, and LIST..." +``` + +Having developed MistKit previously, I understood the challenge of various field types and the difficulty in expressing that in Swift. This is a common challenge in Swift with JSON data. + +Claude's suggestion of [`typeOverrides`](https://swiftpackageindex.com/apple/swift-openapi-generator/1.10.3/documentation/swift-openapi-generator/configuring-the-generator#Type-overrides) was the breakthrough—instead of fighting OpenAPI's type system, we'd let the generator create basic types, then override with our custom implementation that handles CloudKit's quirks. + +#### Understanding ASSET vs ASSETID + +CloudKit uses two different type discriminators for [asset fields](https://developer.apple.com/library/archive/documentation/DataManagement/Conceptual/CloudKitWebServicesReference/Types.html#//apple_ref/doc/uid/TP40015240-CH3-SW2): + +**[ASSET](https://developer.apple.com/library/archive/documentation/DataManagement/Conceptual/CloudKitWebServicesReference/Types.html#//apple_ref/doc/uid/TP40015240-CH3-SW2)** - Full asset metadata returned by CloudKit +- Appears in: Query responses, lookup responses, modification responses +- Contains: `fileChecksum`, `size`, `downloadURL`, `wrappingKey`, `receipt` +- Use case: When you need to download or verify the asset file + +**[ASSETID](https://developer.apple.com/library/archive/documentation/DataManagement/Conceptual/CloudKitWebServicesReference/Types.html#//apple_ref/doc/uid/TP40015240-CH3-SW2)** - Asset reference placeholder +- Appears in: Record creation/update requests +- Contains: Same structure as ASSET, but typically only `downloadURL` populated +- Use case: When you're referencing an already-uploaded asset + +At the end of the day, both decode to the same `AssetValue` structure, but CloudKit distinguishes them with different type strings (`"ASSET"` vs `"ASSETID"`). Our custom implementation handles this elegantly: + +```swift +internal struct CustomFieldValue: Codable, Hashable, Sendable { + internal enum FieldTypePayload: String, Codable, Sendable { + case asset = "ASSET" + case assetid = "ASSETID" // Both decode to AssetValue + case string = "STRING" + case int64 = "INT64" + // ... more types + } + + internal let value: CustomFieldValuePayload + internal let type: FieldTypePayload? +} +``` + +Using the `CustomFieldValue` with the power of openapi-generator `typeOverides` allows us to implement the specific quirks of CloudKit field values. + + +### Challenge #2: Authentication Complexity + +The next challenge was dealing with the 3 different methods of authentication: + +1. **API Token** - Container-level access + - Query parameter: `ckAPIToken` + - Simplest method + - A starting point for **Web Auth Token** + +2. **[Web Auth Token](https://developer.apple.com/library/archive/documentation/DataManagement/Conceptual/CloudKitWebServicesReference/SettingUpWebServices.html#//apple_ref/doc/uid/TP40015240-CH24-SW2)** - User-specific access + - Two query parameters: `ckAPIToken` + `ckWebAuthToken` + - For private database access + +3. **[Server-to-Server](https://developer.apple.com/library/archive/documentation/DataManagement/Conceptual/CloudKitWebServicesReference/SettingUpWebServices.html#//apple_ref/doc/uid/TP40015240-CH24-SW6)** - Public Database Access + - ECDSA P-256 signature in Authorization header + - Most complex, most secure + + +This became a complexity problem when trying to model it in OpenAPI. What Claude suggested was to use the [ClientMiddleware API](https://swiftpackageindex.com/apple/swift-openapi-runtime/1.8.3/documentation/openapiruntime/clientmiddleware) to handle authentication dynamically rather than relying on generator's built-in auth. The meant we used: + +1. **OpenAPI**: Define all three `securitySchemes` but make endpoint security optional (`security: []`) +2. **Middleware**: Implement `AuthenticationMiddleware` that inspects `TokenManager` at runtime +3. **TokenManager Protocol**: Three implementations (API, WebAuth, ServerToServer) +4. **Runtime Selection**: Client chooses auth method via TokenManager injection + +Here's what it looks like: + +```swift +internal struct AuthenticationMiddleware: ClientMiddleware { + internal let tokenManager: any TokenManager + + func intercept(...) async throws -> (HTTPResponse, HTTPBody?) { + guard let credentials = try await tokenManager.getCurrentCredentials() else { + throw TokenManagerError.invalidCredentials(.noCredentialsAvailable) + } + + var modifiedRequest = request + + switch credentials.method { + case .apiToken(let token): + // Add ckAPIToken to query parameters + case .webAuthToken(let apiToken, let webToken): + // Add both tokens to query parameters + case .serverToServer(let keyID, let privateKey): + // Sign request with ECDSA + // Add Authorization header with signature + } + + return try await next(modifiedRequest, body, baseURL) + } +} +``` + +This helps because: + +- ✅ Generator doesn't need to handle auth complexity +- ✅ We control authentication at runtime +- ✅ Easy to test (inject mock TokenManager) +- ✅ Supports all three methods seamlessly +- ✅ Can switch auth methods without code changes + +While this looks like it will work I wanted to make absolute sure. When using an LLM **it's important to have code which actually proves that it works and not just unit test.** So I created a simple command line tool which would to both write and read from a public and private database: + +```no-highlight +Me: "Can you run MistDemo to test the actual functionality?" + +Claude: *[Runs demo successfully]* +"Perfect! The MistDemo ran successfully: + - Web Authentication - Used API token + web auth token + - CloudKit API Calls - Made multiple successful requests + - HTTP Integration - AuthenticationMiddleware properly added all required parameters + - Data Retrieval - Successfully retrieved TodoItem records from CloudKit + + The authentication layer is working as designed. All three authentication methods have a solid foundation!" +``` + +We'll touch more on this approach in [the next blog post.](/tutorials/rebuilding-mistkit-claude-code-part-2/) + + +### Challenge #3: Error Handling + +[CloudKit returns over 9 different HTTP status codes](https://developer.apple.com/library/archive/documentation/DataManagement/Conceptual/CloudKitWebServicesReference/ErrorCodes.html), each with nested error details including `serverErrorCode`, `reason`, `uuid`, and sometimes `redirectURL` or `retryAfter`. What would be nice is if we can parse these in a Swift-y way, taking advantage of Swift 6 features like typed throws for more precise error handling. + +According to Apple's Documentation: + +> **Record Fetch Error Dictionary** +> +> The error dictionary describing a failed operation with the following keys: + + - `recordName`: The name of the record that the operation failed on. + - `reason`: A string indicating the reason for the error. + - `serverErrorCode`: A string containing the code for the error that occurred. For possible values, see Error Codes. + - `retryAfter`: The suggested time to wait before trying this operation again. + - `uuid`: A unique identifier for this error. + - `redirectURL`: A redirect URL for the user to securely sign in. + +Based on this, I had Claude create an openapi entry on this: + +```yaml +components: + schemas: + ErrorResponse: + type: object + description: Error response object + properties: + uuid: + type: string + description: Unique error identifier for support + serverErrorCode: + type: string + enum: + - ACCESS_DENIED + - ATOMIC_ERROR + - AUTHENTICATION_FAILED + - AUTHENTICATION_REQUIRED + - BAD_REQUEST + - CONFLICT + - EXISTS + - INTERNAL_ERROR + - NOT_FOUND + - QUOTA_EXCEEDED + - THROTTLED + - TRY_AGAIN_LATER + - VALIDATING_REFERENCE_ERROR + - ZONE_NOT_FOUND + reason: + type: string + redirectURL: + type: string + + responses: + BadRequest: + description: Bad request (400) - BAD_REQUEST, ATOMIC_ERROR + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + Unauthorized: + description: Unauthorized (401) - AUTHENTICATION_FAILED + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + # ... additional error responses for 403, 404, 409, 412, 413, 421, 429, 500, 503 +``` + +Claude was able to translate the documentation into: + +1. **Error Code Enum**: Converted prose list of error codes to explicit enum +2. **HTTP Status Mapping**: Created reusable response components for each HTTP status +3. **Consistent Schema**: All errors use same `ErrorResponse` schema +4. **Status Documentation**: Linked HTTP statuses to CloudKit error codes in descriptions + +This enables: +- **Type-Safe Error Handling**: Generated code includes all possible error codes +- **Automatic Deserialization**: Errors automatically parsed to correct type +- **Centralized Definitions**: Define once, reference everywhere + +Here's how it's mapped: + +| HTTP Status | CloudKit Error Codes | Client Action | +|-------------|---------------------|---------------| +| **400 Bad Request** | `BAD_REQUEST`, `ATOMIC_ERROR` | Fix request parameters or retry non-atomically | +| **401 Unauthorized** | `AUTHENTICATION_FAILED` | Re-authenticate or check credentials | +| **403 Forbidden** | `ACCESS_DENIED` | User lacks permissions | +| **404 Not Found** | `NOT_FOUND`, `ZONE_NOT_FOUND` | Verify resource exists | +| **409 Conflict** | `CONFLICT`, `EXISTS` | Fetch latest version and retry, or use force operations | +| **412 Precondition Failed** | `VALIDATING_REFERENCE_ERROR` | Referenced record doesn't exist | +| **413 Request Too Large** | `QUOTA_EXCEEDED` | Reduce request size or upgrade quota | +| **429 Too Many Requests** | `THROTTLED` | Implement exponential backoff | +| **500 Internal Error** | `INTERNAL_ERROR` | Retry with backoff | +| **503 Service Unavailable** | `TRY_AGAIN_LATER` | Temporary issue, retry later | + +This structured [error handling](https://brightdigit.com/articles/swift-error-handling/) enables the generated client to provide specific, actionable error messages rather than generic HTTP failures. Developers get type-safe error codes, HTTP status mapping, and clear guidance on how to handle each error condition. + + +### Challenge #4: API Ergonomics + +The generated OpenAPI client works, but it's not exactly ergonomic. Here's what a simple query looks like with the raw generated code: + +```swift +// Verbose generated API +let input = Operations.queryRecords.Input( + path: .init( + version: "1", + container: "iCloud.com.example.MyApp", + environment: Components.Parameters.environment.production, + database: Components.Parameters.database._private + ), + headers: .init(accept: [.json]), + body: .json(.init( + query: .init(recordType: "User") + )) +) + +let response = try await client.queryRecords(input) + +switch response { +case .ok(let okResponse): + let queryResponse = try okResponse.body.json + // Process records... +default: + // Handle errors... +} +``` + +The problem is there's too much boilerplate for simple operations when we can clean this up with a nicer abstraction. The solution was to build a three-layer architecture that keeps the generated code internal and exposes a clean public API: + +Three-layer architecture showing User Code (public API), MistKit Abstraction (internal), and Generated OpenAPI Client (internal) + +So now it can look something like this: + +```swift +// Clean, idiomatic Swift +let service = try CloudKitService( + container: "iCloud.com.example.MyApp", + environment: .production, + database: .private, + tokenManager: tokenManager +) + +let records = try await service.queryRecords( + recordType: "User", + filter: .equals("status", .string("active")) +) + +// Type-safe field access +for record in records { + if let name = record.fields["name"]?.stringValue { + print("User: \(name)") + } +} +``` + +In this case, we create a few abstraction to help: + +- `FieldValue` enum for type-safe field access +- `RecordInfo` struct for read operations +- `QueryFilter` for building queries +- `CloudKitService` wrapper hiding OpenAPI complexity + +This means the generated code stays internal while users interact with the more friendly API. + + +### The Iterative Workflow with Claude + +This process of building and refining was iterative when working with Claude Code: + +1. **I draft the structure** + - Provide CloudKit domain knowledge and desired API + +2. **Claude expands** + - Fills in request/response schemas + - Generates boilerplate for similar endpoints + - Creates consistent patterns + +3. **I review for CloudKit accuracy** + - Check against Apple docs + - Add edge cases and CloudKit quirks + - Refine error responses + - Define integration and unit tests for verification + +4. **Claude validates consistency** + - Catches schema mismatches + - Suggests improvements + +5. **Iterate until complete** + +Let's take for instance, this conversation I had with Claude: + +```no-highlight +Me: "Here's the query endpoint from Apple's docs" + +Claude: *[Creates complete OpenAPI definition]* +"Here's a complete OpenAPI definition with request/response schemas" + +Me: "Add `resultsLimit` validation and `continuationMarker` for pagination" + +Claude: *[Updates definition with pagination support]* +"Updated, and I noticed the `zoneID` should be optional" +``` + +> youtube https://youtu.be/gH3QnVHsUAc + +By providing my own experience with great Swift APIs and Claude's ability at applying patterns, I quickly build a library that's friendly to use. + +#### Building MistKit from Scratch with Claude Code + +With Claude Code, I could easily create an openapi document based on the Apple's documentation. With my guidance and understanding with the REST API and good Swift design, I could guide Claude through issues like: + +* Field Value with the oneOf pattern and handling the ASSETID quirk) +* completed authentication modeling with three security schemes + +This will make it much easier to continue future features with MistKit and enabling me to create some server-side application for my apps. + + +## What's Next + +After three months of collaboration with Claude (**representing significant acceleration over manual development**), I had: +- ✅ 10,476 lines of generated, type-safe Swift code +- ✅ Three authentication methods working seamlessly +- ✅ CustomFieldValue handling CloudKit's polymorphic types +- ✅ Clean public API hiding OpenAPI complexity +- ✅ 161 tests across 47 test files + +The OpenAPI spec was complete. The generated client compiled. The abstraction layer was elegant. Unit tests passed. + +**How Claude Code Accelerated Development:** +- **Documentation Translation**: Converting Apple's prose documentation to a precise OpenAPI spec would have taken weeks manually. Claude handled the bulk of this in days, with me providing CloudKit domain expertise and corrections. +- **Boilerplate Generation**: The 10,476 lines of generated Swift code from swift-openapi-generator saved months of hand-writing networking code, request/response types, and JSON handling. +- **Pattern Application**: Once I established patterns (like `CustomFieldValue` for polymorphic types), Claude consistently applied them across the codebase. +- **Iteration Speed**: When authentication approaches needed refactoring, Claude could update dozens of files in minutes vs. hours of manual editing. + +What would have likely taken 6-12 months of solo development was compressed into 3 months of _side-project_ collaboration, with Claude handling repetitive tasks while I focused on architecture, CloudKit-specific quirks, and real-world testing. + +However I really needed to put it the test in my actual uses. In the next post, I'll talk about find flaws in MistKit by actually consuming my library with help from Claude Code. I'll be building a couple of command line tools for easily uploading data for [Bushel](https://getbushel.app) and a future RSS Reader to the public database. By doing this I'll understand [Claude's limitation, benefits and how to workaround those.](/tutorials/rebuilding-mistkit-claude-code-part-2/) diff --git a/docs/cloudkit-guide/articles/rebuilding-mistkit-claude-code-part-2.md b/docs/cloudkit-guide/articles/rebuilding-mistkit-claude-code-part-2.md new file mode 100644 index 00000000..129c56ef --- /dev/null +++ b/docs/cloudkit-guide/articles/rebuilding-mistkit-claude-code-part-2.md @@ -0,0 +1,195 @@ +--- +title: Rebuilding MistKit with Claude Code - Real-World Lessons and Collaboration Patterns (Part 2) +date: 2025-12-10 00:00 +description: After building MistKit's type-safe CloudKit client, we put it to the test with real applications. Discover what happened when theory met practice—the unexpected discoveries, hard-earned lessons, and collaboration patterns that emerged from 428 Claude Code sessions over three months. +featuredImage: /media/tutorials/rebuilding-mistkit-claude-code/mistkit-rebuild-part1-hero.webp +subscriptionCTA: Want to learn more about AI-assisted Swift development and modern API design patterns? Sign up for our newsletter to get notified about the rest of the Modern Swift Patterns series and future tutorials on building production-ready Swift applications. +--- + +In [Part 1](https://brightdigit.com/tutorials/rebuilding-mistkit-claude-code-part-1/), I showed how [Claude Code](https://claude.ai/claude-code) and [swift-openapi-generator](https://github.com/apple/swift-openapi-generator) transformed [CloudKit's REST documentation](https://developer.apple.com/documentation/cloudkitjs/cloudkit/cloudkit_web_services) into a type-safe Swift client. We had 161 unit tests which passed, but would it actually work in the real world? + +📚 **[View Documentation](https://swiftpackageindex.com/brightdigit/MistKit/documentation)** | 🐙 **[GitHub Repository](https://github.com/brightdigit/MistKit)** + +- [Real-World Proof](#real-world-proof) + - [The Celestra and Bushel Examples](#the-celestra-and-bushel-examples) + - [Integration Testing Through Real Applications](#integration-testing-through-real-applications) +- [Lessons Learned](#lessons-learned) + - [Unit Test Generation](#unit-test-generation) + - [Human Guided Architecture](#human-guided-architecture) + - [Grabby AI](#grabby-ai) + - [Context Management](#context-management) + - [Human + AI Code Reviews](#human--ai-code-reviews) +- [Multiplier, not a Replacement](#multiplier-not-a-replacement) + + +## Real-World Proof + +Would MistKit's abstractions actually work when building an application? I had 2 real-world applications for MistKit to try it out: + +- an RSS aggregator syncing thousands of articles to CloudKit using [SyndiKit](https://github.com/brightdigit/SyndiKit) for an app codenamed **[Celestra](https://celestr.app)** +- For **[Bushel](https://getbushel.app)**, I wanted to track restore images and various metadata for macOS and developer software versions. + + + +### The Celestra and Bushel Examples + +Tests validate correctness, but real applications validate design. MistKit needed to prove it could power actual software and not just pass unit tests. Enter two real-world applications—**[the Celestra app](https://celestr.app)** (an RSS reader) and **[the Bushel app](https://getbushel.app)** (a macOS virtualization tool)—each powered by MistKit-driven CLI backends that populate CloudKit public databases. These CLI tools, running on scheduled cloud infrastructure, proved MistKit works in production. + +The architecture for both follows the same pattern: +- **Consumer apps** ([the Celestra app](https://celestr.app), [the Bushel app](https://getbushel.app)) - iOS/macOS apps that read from CloudKit +- **CLI tools** - Built with MistKit, run on cloud infrastructure (cron jobs, cloud functions, scheduled tasks) +- **CloudKit public database** - Central data layer connecting CLI tools to apps + +This pattern enables: +- **Automated updates**: CLI tools run on schedules without user devices being online +- **Separation of concerns**: Data population (CLI) vs data consumption (app) +- **Scalability**: Cloud infrastructure handles data aggregation, apps stay lightweight + +#### Celestra: Automated RSS Feed Sync for a Reader App + +The [Celestra app](https://celestr.app) is an RSS reader in development for iOS and macOS. To keep content fresh without requiring the app to be open, I built a [CLI tool with MistKit](https://github.com/brightdigit/MistKit/tree/main/Examples/Celestra) that runs on scheduled cloud infrastructure. The CLI tool runs periodically (cron job, cloud function, scheduled task) to fetch RSS feeds and sync them to CloudKit's public database, making fresh content available to all users instantly—even when their devices are offline. + +This architecture enables push notifications on updated articles without the app running, and MistKit's batch operations can efficiently handle hundreds of content updates. The [CLI tool example](https://github.com/brightdigit/MistKit/tree/main/Examples/Celestra) demonstrates key MistKit patterns: + +**Query filtering** - Find feeds that need updating: +```swift +// Query filtering - find stale feeds +QueryFilter.lessThan("lastAttempted", .date(cutoff)) +QueryFilter.greaterThanOrEquals("usageCount", .int64(minPop)) +``` + +**Batch operations** - Efficiently sync hundreds of articles: +```swift +// Batch operations +let operations = articles.map { article in + RecordOperation.create( + recordType: "Article", + recordName: article.guid, + fields: article.toCloudKitFields() + ) +} +service.modifyRecords(operations, atomic: false) +``` + +#### Bushel: Powering a macOS VM App with CloudKit + +The [Bushel app](https://getbushel.app) is a macOS virtualization tool for developers. It currently allows pluggable _hubs_ to get a list of restore images, their download URLs, and their status. However, since the data is universal, I wanted a comprehensive, queryable central database of macOS restore images and various metadata about operating system versions and developer tools. Therefore I wanted a [CLI tool with MistKit](https://github.com/brightdigit/MistKit/tree/main/Examples/Bushel) that runs on scheduled cloud infrastructure (cron jobs, cloud functions, scheduled tasks) to populate a CloudKit public database with various metadata about macOS versions and their restore images. + +This architecture provides: +- **Public Database**: Worldwide access to version history without embedding static JSON in the app +- **Automated Updates**: CLI tool syncs latest info on restore images, Xcode, and Swift versions +- **Queryable**: [Bushel app](https://getbushel.app) can easily query for restore images such as _macOS 15.2_ +- **Scalable**: CLI tool aggregates data from various sources automatically +- **Deduplication**: buildNumber-based deduplication ensures clean data + +The [CLI tool example](https://github.com/brightdigit/MistKit/tree/main/Examples/Bushel) demonstrates advanced MistKit patterns: + +```swift +// Protocol-based record conversion +protocol CloudKitRecord { + static var cloudKitRecordType: String { get } + func toCloudKitFields() -> [String: FieldValue] +} + +// Relationship handling +fields["minimumMacOS"] = .reference( + Reference(recordName: restoreImageRecordName) +) +``` + +--- + +> transistor https://share.transistor.fm/s/ffcb9fc1 + +Both CLI tool examples serve as copy-paste starting points for new MistKit projects. + + +Watching MistKit power real applications was satisfying—I could see the generated code actually work in production. The CLI tools successfully synced RSS articles (Celestra) and tracked complex version relationships (Bushel). But more importantly, these real-world applications revealed critical issues that no unit test could catch. + + +### Integration Testing Through Real Applications + +Building real applications exposed issues no unit test could catch. Here's what Celestra and Bushel revealed: + +- **Batch Operation Limits**: CloudKit enforces 200-operation-per-request limit (not documented clearly) therefore we added chunking logic. +- **Boolean Field Handling**: CloudKit has no native boolean type but we wanted the developer to safely use Swift Boolean types so we created a safe and easy way to use the standardized INT64 representation. + +I was able to verify that the API design and critical pieces like Server-to-Server authentication were working as intended. These real-world tests validated MistKit's foundation. + + + +## Lessons Learned + +There were few things which surprised me as far as what Claude Code was good and not good at and where [collaboration worked best](https://brightdigit.com/tutorials/syntaxkit-swift-code-generation/). Claude excelled at tasks I thought would be hard (comprehensive test generation). It struggled with things I assumed would be easy (knowing which APIs exist). Through iteration, a collaboration pattern emerged. Here's what I learned. + +### Unit Test Generation + +Test generation proved to be Claude Code's greatest strength, and it's ideal for LLMs because it leverages pattern recognition from vast training data. Claude has seen thousands of Swift test files and can apply those structural patterns to new domains. The task requires extrapolation from examples, not novel reasoning—Claude recognizes "if testing STRING type, also test empty string, Unicode, and nil" because that pattern appears frequently in its training corpus. A typical interaction looked like this: + +```no-highlight +Me: "Generate tests for all CustomFieldValue types" + +Claude: *[Creates tests covering all 10 field types with edge cases]* +"I've generated comprehensive tests covering: + - All 10 field types (STRING, INT64, DOUBLE, TIMESTAMP, BYTES, REFERENCE, ASSET, ASSETID, LOCATION, LIST) + - Edge cases: empty lists, nil values, malformed data + - Encoding/decoding round-trips + - Nested LIST types + - Invalid type/value combinations + + I also added tests for the ASSET vs ASSETID quirk" +``` + +It was able to produce 161 tests across 47 files including edge cases I hadn't considered. The only quirk I found was that it favored [XCTest](https://developer.apple.com/documentation/xctest) over [Swift Testing](https://developer.apple.com/documentation/testing) at first. This makes sense since there's probably more training material in XCTest. I've primarily switched to Swift Testing for my new work. If you are in the same place then be sure to make a note of that in your `CLAUDE.md` when you start your project. + +### Human Guided Architecture + +While Claude excelled at pattern-based tasks, architectural decisions consistently required human judgment. At various points, Claude would steer the architecture in strange directions that didn't seem correct. The issue is that its training is best for smaller contexts and code examples, which isn't enough for holistic system design. Be confident in steering Claude in the right direction—this is where developer expertise matters most. The risk is drift if the pattern isn't perfectly specified, but for well-defined transformations, LLMs excel. Luckily, Claude does a fairly good job at refactoring when corrected, and its context window (200K tokens in Sonnet 4.5) allows it to see multiple files simultaneously and apply consistent transformations across the codebase. + +### Grabby AI + +These limitations manifested in predictable patterns throughout the project. As we were implementing the CLI tools for Bushel and Celestra, Claude would often try to implement features using the direct [OpenAPI](https://www.openapis.org/) code as opposed to the abstracted API we had built: + +```swift +// WRONG: Internal type reference +let operation = Components.Schemas.RecordOperation( + recordType: "RestoreImage", + fields: fields +) +``` + +Even going so far as to make those methods and properties `public`. Often referred to as power-grabbing, it would go outside its designated boundary, even though I would tell it often not to use those APIs. It's important to set those constraints clearly within the context window and review the code intentionally. All mistakes share common traits—Claude follows patterns from training data or generated code literally without questioning ergonomics or existence. The fix is always the same: explicit guidance in prompts and immediate verification of suggestions. + +### Context Management + +Managing these challenges required strategic context management. One of the biggest challenges working with Claude Code is managing its knowledge cutoffs and lack of familiarity with newer or niche APIs. In the world of Swift, Claude's training often predates [Swift Testing](https://developer.apple.com/documentation/testing) or [swift-openapi-generator](https://github.com/apple/swift-openapi-generator) specifics. This is where providing documentation upfront in `.claude/docs/` helps. With tools like [Sosumi.ai](https://sosumi.ai) for Apple API exploration and [llm.codes](https://llm.codes) I can provide documentation like: +- `testing-enablinganddisabling.md` (126KB) - Swift Testing patterns +- `webservices.md` (289KB) - CloudKit Web Services REST API reference +- `cloudkitjs.md` (188KB) - CloudKit operation patterns and data types +- `swift-openapi-generator.md` (235KB) - Code generation configuration + +> youtube https://youtu.be/gH3QnVHsUAc + +At the root of this is the `CLAUDE.md` file which acts as a table of contents, telling Claude where to look for specific information. Claude doesn't need to memorize everything—it needs to know where to look. + +### Human + AI Code Reviews + +Whatever your AI writes should be understood by you fairly well. Don't skip this step. This is especially important in the context of [humane code](https://brightdigit.com/articles/humane-code/)—code that is empathetic to future developers who need to understand and maintain it. AI-generated code still needs to communicate clearly with the humans who will work with it later. + +> transistor https://share.transistor.fm/s/99f236b1 + +These patterns and practices reflect a deeper truth about AI-assisted development: Claude Code is a force multiplier, not a replacement for developer judgment. I provided architectural vision; Claude generated comprehensive implementations. I identified edge cases from domain knowledge; Claude translated them into exhaustive test suites. I steered strategic decisions; Claude handled mechanical transformations at scale. Together, we built something neither could have built alone—a production-ready CloudKit client that balances type safety with developer ergonomics. + + +## Multiplier, not a Replacement + +These lessons crystallized into a philosophy: **AI is a force multiplier, not a replacement**. Claude generated thousands of lines of code, but I architected what those lines should accomplish. It drafted comprehensive tests, but I knew which edge cases mattered. It refactored at scale, but I chose the patterns worth preserving. Where I lacked expertise translating CloudKit's REST API into an OpenAPI spec, Claude filled those gaps. + +The proof came from real-world application. Building **Celestra** and **Bushel** validated MistKit's design beyond what unit tests could achieve. The CLI tools exposed batch operation limits, revealed boolean field handling quirks, and confirmed that Server-to-Server authentication worked in production. These discoveries transformed MistKit from a technically correct library into a production-ready tool. + +Both CLI examples are now open source as starting points for new projects: +- [Bushel CLI Example](https://github.com/brightdigit/MistKit/tree/main/Examples/Bushel) - Demonstrates complex CloudKit relationships and batch operations powering the [Bushel app](https://getbushel.app) +- [Celestra CLI Example](https://github.com/brightdigit/MistKit/tree/main/Examples/Celestra) - Demonstrates public database patterns and automated sync for the [Celestra app](https://celestr.app) + +Through 428 sessions across three months, Claude Code and I built MistKit v1.0 Alpha—a type-safe CloudKit client that proves AI-assisted development can deliver production-quality Swift libraries when human judgment guides the process. + diff --git a/docs/talk-feedback.md b/docs/talk-feedback.md new file mode 100644 index 00000000..d58b3988 --- /dev/null +++ b/docs/talk-feedback.md @@ -0,0 +1,63 @@ +# Talk Feedback — CloudKit as Your Backend (dry run, 2026-05-05) + +Notes from the Riverside dry run with Evan and Josh. The Keynote deck lives outside the repo, so this file is the durable home for talk-level feedback. Sibling to [`why-mistkit.md`](why-mistkit.md). + +## Source + +- Riverside dry run with Evan + Josh +- Raw transcript: [`transcriptions/transcript.txt`](transcriptions/transcript.txt) +- Self-reported deck completeness during the run: ~60% + +## What's Working + +- The **Heart Witch** Apple-Watch origin story (no login on a watch face → CloudKit) is the single best hook in the deck. +- The **"two and a half authentication methods"** framing is sharper than Apple's three-equal-methods presentation. API Token alone barely qualifies as a method — it's a prerequisite. +- The **GitHub Actions / Bushel / Celestra deployment story** is the strongest section. It's the part of the talk that does not exist in Apple's docs anywhere. +- The **CloudKit Dashboard walkthrough** (Tokens & Keys → openssl command → paste public key → done) is concrete and audience-friendly. + +## Structural Changes + +- **Open with Heart Witch.** Currently it shows up roughly five minutes in. Lead with the problem ("watch user can't type a password"), not company background. +- **Make the public-vs-private + auth-method a 2D matrix slide**, not a bullet list. It's the structure people will remember. +- **Move the deployment / GitHub Actions section earlier** and give it more time. It's defensible content; the intro is not. +- **Fold API Token into the Web Auth Token section.** "Two and a half" is honest framing for the intro, but a full slide on it is overkill — it's a prerequisite, not a peer method. + +## Cuts + +- **General CloudKit / NoSQL intro** — covered by the Part 1 article; assume the audience. +- **MistKit origin / Claude-Code rebuild deep dive** — that's Part 1/2 article territory; one slide max. +- **Field-type polymorphism deep dive** — same; ~30 seconds. +- **Error-handling deep dive** — same; ~30 seconds. +- **WASM / browser-extension tangent** — off-topic for backend services. Replace with one line: "running in a browser? Use CloudKit JS, not MistKit." +- **Roadmap / "what's next" closing** — the Part 2 article covers it; keep one slide for "where to follow along." + +## Expand / Add + +- **`CKFetchWebAuthTokenOperation`** — the iOS-app-to-backend handoff path. Audience members building iOS+server stacks will ask about it in Q&A. At minimum one slide saying "the other way to get a web auth token is from inside an iOS app via `CKFetchWebAuthTokenOperation`; haven't shipped this pattern personally but here's the documented flow." +- **The signing payload format** — the talk hand-waves "Claude figured it out from the docs." Show the canonical string (HTTP method + ISO 8601 timestamp + SHA-256 body hash + path) and the `Authorization` header format. Pull straight from `Sources/MistKit/Service/AuthenticationMiddleware.swift`. +- **Web-auth-token lifetime / refresh** — one bullet. If unknown, spend 15 minutes in the dashboard before the live talk. + +## Audio / Delivery Cleanup (for the recorded version) + +Lines from the transcript to clean up: + +- "Sorry, just going into Do Not Disturb mode." +- "I hate Teams." +- "Surprised? I mean, I know they have an app." +- Multiple "sorry, slides aren't done" asides — replace with confidence in the recorded take. + +## Brand / Spelling + +The auto-transcription introduces several errors that would propagate if the transcript is fed back into Claude as source material: + +- "Heart Witch" → mangled as "Hart Twitch" / "Hardwitch" throughout. Confirm the on-screen spelling and the slide title before recording. +- "MistKit" → consistently transcribed as "Miskit." Search-and-replace before reuse. +- Around the WASM tangent (line 135), "WASM" gets transcribed as "awesome." + +## Q&A Prep — Likely Audience Questions + +- **"How do I get a web auth token from inside my iOS app?"** → `CKFetchWebAuthTokenOperation`. (See *Expand* above.) +- **"Can I use this from a browser extension?"** → Yes for non-Safari, but use CloudKit JS unless you specifically need Swift. +- **"What's the production story for key storage?"** → GitHub Actions Secrets in Bushel / Celestra; secrets manager or env-var injection in general. +- **"Does this work on Linux?"** → Yes — that's the whole point. Also Windows and Android. Not WASM yet (no transport). +- **"How does this compare to using Vapor + the CloudKit framework?"** → The CloudKit framework only runs on Apple platforms. MistKit runs anywhere Swift runs. diff --git a/docs/transcriptions/paragraphs.json b/docs/transcriptions/paragraphs.json new file mode 100644 index 00000000..55b33754 --- /dev/null +++ b/docs/transcriptions/paragraphs.json @@ -0,0 +1 @@ +{"paragraphs":[{"text":"Hey, Evan, can you hear me all right? Yeah, I can hear you. Awesome. How do I sound? Good.","start":262980,"end":268740,"confidence":0.99658203,"words":[{"text":"Hey,","start":262980,"end":263180,"confidence":0.99658203,"speaker":"A"},{"text":"Evan,","start":263180,"end":263580,"confidence":0.99609375,"speaker":"A"},{"text":"can","start":263580,"end":263700,"confidence":0.9995117,"speaker":"A"},{"text":"you","start":263700,"end":263780,"confidence":0.99316406,"speaker":"A"},{"text":"hear","start":263780,"end":263900,"confidence":1,"speaker":"A"},{"text":"me","start":263900,"end":264020,"confidence":1,"speaker":"A"},{"text":"all","start":264020,"end":264140,"confidence":0.87158203,"speaker":"A"},{"text":"right?","start":264140,"end":264420,"confidence":0.96240234,"speaker":"A"},{"text":"Yeah,","start":264660,"end":265020,"confidence":0.9741211,"speaker":"B"},{"text":"I","start":265020,"end":265140,"confidence":1,"speaker":"B"},{"text":"can","start":265140,"end":265260,"confidence":1,"speaker":"B"},{"text":"hear","start":265260,"end":265420,"confidence":1,"speaker":"B"},{"text":"you.","start":265420,"end":265700,"confidence":0.99365234,"speaker":"B"},{"text":"Awesome.","start":266420,"end":267060,"confidence":0.9998372,"speaker":"A"},{"text":"How","start":267060,"end":267340,"confidence":1,"speaker":"A"},{"text":"do","start":267340,"end":267500,"confidence":1,"speaker":"A"},{"text":"I","start":267500,"end":267660,"confidence":1,"speaker":"A"},{"text":"sound?","start":267660,"end":268020,"confidence":0.99975586,"speaker":"A"},{"text":"Good.","start":268340,"end":268740,"confidence":0.99902344,"speaker":"A"}]},{"text":"I've used this microphone in ages. It's like all dusty.","start":270260,"end":274420,"confidence":0.7714844,"words":[{"text":"I've","start":270260,"end":270740,"confidence":0.7714844,"speaker":"A"},{"text":"used","start":270740,"end":270940,"confidence":0.99316406,"speaker":"A"},{"text":"this","start":270940,"end":271140,"confidence":0.9736328,"speaker":"A"},{"text":"microphone","start":271140,"end":271660,"confidence":0.9484375,"speaker":"A"},{"text":"in","start":271660,"end":271820,"confidence":0.9946289,"speaker":"A"},{"text":"ages.","start":271820,"end":272340,"confidence":0.9995117,"speaker":"A"},{"text":"It's","start":273060,"end":273420,"confidence":0.99397784,"speaker":"A"},{"text":"like","start":273420,"end":273580,"confidence":0.99121094,"speaker":"A"},{"text":"all","start":273580,"end":273780,"confidence":0.98583984,"speaker":"A"},{"text":"dusty.","start":273780,"end":274420,"confidence":0.99934894,"speaker":"A"}]},{"text":"How you think I should wait like five minutes for people to come in or. Probably. Yeah, that there's if. Yeah, otherwise you can just. You could start, but that'll be interesting.","start":281140,"end":291930,"confidence":0.6699219,"words":[{"text":"How","start":281140,"end":281500,"confidence":0.6699219,"speaker":"A"},{"text":"you","start":281500,"end":281700,"confidence":0.97021484,"speaker":"A"},{"text":"think","start":281700,"end":281820,"confidence":1,"speaker":"A"},{"text":"I","start":281820,"end":281940,"confidence":0.99853516,"speaker":"A"},{"text":"should","start":281940,"end":282060,"confidence":0.9995117,"speaker":"A"},{"text":"wait","start":282060,"end":282260,"confidence":0.99975586,"speaker":"A"},{"text":"like","start":282260,"end":282380,"confidence":0.99316406,"speaker":"A"},{"text":"five","start":282380,"end":282540,"confidence":0.9995117,"speaker":"A"},{"text":"minutes","start":282540,"end":282820,"confidence":1,"speaker":"A"},{"text":"for","start":282820,"end":283020,"confidence":0.9995117,"speaker":"A"},{"text":"people","start":283020,"end":283220,"confidence":0.9995117,"speaker":"A"},{"text":"to","start":283220,"end":283380,"confidence":0.9916992,"speaker":"A"},{"text":"come","start":283380,"end":283540,"confidence":0.9995117,"speaker":"A"},{"text":"in","start":283540,"end":283780,"confidence":0.99902344,"speaker":"A"},{"text":"or.","start":283780,"end":284100,"confidence":0.9394531,"speaker":"A"},{"text":"Probably.","start":284260,"end":284740,"confidence":0.8670247,"speaker":"B"},{"text":"Yeah,","start":284980,"end":285460,"confidence":0.99316406,"speaker":"B"},{"text":"that","start":285770,"end":285970,"confidence":0.72314453,"speaker":"B"},{"text":"there's","start":285970,"end":286410,"confidence":0.8248698,"speaker":"B"},{"text":"if.","start":286490,"end":286890,"confidence":0.97558594,"speaker":"B"},{"text":"Yeah,","start":286970,"end":287530,"confidence":0.99869794,"speaker":"B"},{"text":"otherwise","start":288010,"end":288450,"confidence":0.98502606,"speaker":"B"},{"text":"you","start":288450,"end":288570,"confidence":0.99902344,"speaker":"B"},{"text":"can","start":288570,"end":288690,"confidence":0.99902344,"speaker":"B"},{"text":"just.","start":288690,"end":288890,"confidence":1,"speaker":"B"},{"text":"You","start":288890,"end":289090,"confidence":0.99609375,"speaker":"B"},{"text":"could","start":289090,"end":289290,"confidence":0.9824219,"speaker":"B"},{"text":"start,","start":289290,"end":289610,"confidence":0.9995117,"speaker":"B"},{"text":"but","start":289850,"end":290250,"confidence":0.99902344,"speaker":"B"},{"text":"that'll","start":291130,"end":291530,"confidence":0.96761066,"speaker":"B"},{"text":"be","start":291530,"end":291610,"confidence":0.9995117,"speaker":"B"},{"text":"interesting.","start":291610,"end":291930,"confidence":0.99609375,"speaker":"B"}]},{"text":"Do you mind if I grab a cup of coffee real quick? No, not at all. Not at all. Okay, cool. I'm not using the AirPods mic, so I can hear you, but you won't be able to hear me.","start":291930,"end":301370,"confidence":0.7919922,"words":[{"text":"Do","start":291930,"end":292090,"confidence":0.7919922,"speaker":"A"},{"text":"you","start":292090,"end":292170,"confidence":0.99560547,"speaker":"A"},{"text":"mind","start":292170,"end":292290,"confidence":0.9995117,"speaker":"A"},{"text":"if","start":292290,"end":292450,"confidence":0.99560547,"speaker":"A"},{"text":"I","start":292450,"end":292650,"confidence":0.9995117,"speaker":"A"},{"text":"grab","start":292650,"end":292930,"confidence":1,"speaker":"A"},{"text":"a","start":292930,"end":293050,"confidence":0.9995117,"speaker":"A"},{"text":"cup","start":293050,"end":293170,"confidence":0.9995117,"speaker":"A"},{"text":"of","start":293170,"end":293330,"confidence":0.9970703,"speaker":"A"},{"text":"coffee","start":293330,"end":293650,"confidence":0.9998372,"speaker":"A"},{"text":"real","start":293650,"end":293810,"confidence":0.9995117,"speaker":"A"},{"text":"quick?","start":293810,"end":294010,"confidence":1,"speaker":"A"},{"text":"No,","start":294010,"end":294250,"confidence":0.9975586,"speaker":"B"},{"text":"not","start":294250,"end":294450,"confidence":1,"speaker":"B"},{"text":"at","start":294450,"end":294570,"confidence":0.9995117,"speaker":"B"},{"text":"all.","start":294570,"end":294730,"confidence":1,"speaker":"B"},{"text":"Not","start":294730,"end":294930,"confidence":0.71875,"speaker":"A"},{"text":"at","start":294930,"end":295010,"confidence":0.8486328,"speaker":"A"},{"text":"all.","start":295010,"end":295210,"confidence":0.9042969,"speaker":"A"},{"text":"Okay,","start":295530,"end":296090,"confidence":0.9946289,"speaker":"A"},{"text":"cool.","start":296730,"end":297210,"confidence":0.99609375,"speaker":"A"},{"text":"I'm","start":297210,"end":297570,"confidence":0.8929036,"speaker":"A"},{"text":"not","start":297570,"end":297730,"confidence":1,"speaker":"A"},{"text":"using","start":297730,"end":297930,"confidence":0.9995117,"speaker":"A"},{"text":"the","start":297930,"end":298090,"confidence":0.99609375,"speaker":"A"},{"text":"AirPods","start":298090,"end":298610,"confidence":0.96594,"speaker":"A"},{"text":"mic,","start":298610,"end":298930,"confidence":0.9863281,"speaker":"A"},{"text":"so","start":298930,"end":299250,"confidence":0.99902344,"speaker":"A"},{"text":"I","start":299250,"end":299490,"confidence":1,"speaker":"A"},{"text":"can","start":299490,"end":299650,"confidence":0.9995117,"speaker":"A"},{"text":"hear","start":299650,"end":299810,"confidence":1,"speaker":"A"},{"text":"you,","start":299810,"end":299970,"confidence":0.9995117,"speaker":"A"},{"text":"but","start":299970,"end":300130,"confidence":1,"speaker":"A"},{"text":"you","start":300130,"end":300290,"confidence":1,"speaker":"A"},{"text":"won't","start":300290,"end":300490,"confidence":0.9998372,"speaker":"A"},{"text":"be","start":300490,"end":300570,"confidence":1,"speaker":"A"},{"text":"able","start":300570,"end":300690,"confidence":1,"speaker":"A"},{"text":"to","start":300690,"end":300850,"confidence":1,"speaker":"A"},{"text":"hear","start":300850,"end":301050,"confidence":0.9995117,"speaker":"A"},{"text":"me.","start":301050,"end":301370,"confidence":0.9995117,"speaker":"A"}]},{"text":"Okay.","start":301690,"end":302250,"confidence":0.98746747,"words":[{"text":"Okay.","start":301690,"end":302250,"confidence":0.98746747,"speaker":"B"}]},{"text":"It's.","start":362440,"end":387820,"confidence":0.7732747,"words":[{"text":"It's.","start":362440,"end":387820,"confidence":0.7732747,"speaker":"A"}]},{"text":"Thank you for your patience.","start":531699,"end":535060,"confidence":0.9851074,"words":[{"text":"Thank","start":531699,"end":531940,"confidence":0.9851074,"speaker":"A"},{"text":"you","start":531940,"end":532260,"confidence":1,"speaker":"A"},{"text":"for","start":533860,"end":534220,"confidence":0.59277344,"speaker":"A"},{"text":"your","start":534220,"end":534500,"confidence":1,"speaker":"A"},{"text":"patience.","start":534500,"end":535060,"confidence":0.9992676,"speaker":"A"}]},{"text":"So is it just you? It looks like it's just me. Josh is trying to get in, but he's trying to get on on his mobile device and I don't think that's possible with Riverside.","start":549010,"end":559250,"confidence":0.9873047,"words":[{"text":"So","start":549010,"end":549130,"confidence":0.9873047,"speaker":"A"},{"text":"is","start":549130,"end":549290,"confidence":0.99365234,"speaker":"A"},{"text":"it","start":549290,"end":549450,"confidence":0.99902344,"speaker":"A"},{"text":"just","start":549450,"end":549650,"confidence":1,"speaker":"A"},{"text":"you?","start":549650,"end":549970,"confidence":0.9995117,"speaker":"A"},{"text":"It","start":551330,"end":551610,"confidence":0.95751953,"speaker":"B"},{"text":"looks","start":551610,"end":551810,"confidence":1,"speaker":"B"},{"text":"like","start":551810,"end":551930,"confidence":0.9995117,"speaker":"B"},{"text":"it's","start":551930,"end":552130,"confidence":0.9996745,"speaker":"B"},{"text":"just","start":552130,"end":552290,"confidence":1,"speaker":"B"},{"text":"me.","start":552290,"end":552570,"confidence":1,"speaker":"B"},{"text":"Josh","start":552570,"end":553010,"confidence":0.9995117,"speaker":"B"},{"text":"is","start":553010,"end":553290,"confidence":0.9970703,"speaker":"B"},{"text":"trying","start":553290,"end":553530,"confidence":0.9995117,"speaker":"B"},{"text":"to","start":553530,"end":553650,"confidence":1,"speaker":"B"},{"text":"get","start":553650,"end":553810,"confidence":1,"speaker":"B"},{"text":"in,","start":553810,"end":554010,"confidence":0.9995117,"speaker":"B"},{"text":"but","start":554010,"end":554170,"confidence":0.9995117,"speaker":"B"},{"text":"he's","start":554170,"end":554610,"confidence":0.92529297,"speaker":"B"},{"text":"trying","start":554610,"end":554930,"confidence":0.9995117,"speaker":"B"},{"text":"to","start":554930,"end":555090,"confidence":1,"speaker":"B"},{"text":"get","start":555090,"end":555210,"confidence":1,"speaker":"B"},{"text":"on","start":555210,"end":555490,"confidence":0.9272461,"speaker":"B"},{"text":"on","start":555650,"end":555970,"confidence":1,"speaker":"B"},{"text":"his","start":555970,"end":556210,"confidence":0.99902344,"speaker":"B"},{"text":"mobile","start":556210,"end":556530,"confidence":0.9998372,"speaker":"B"},{"text":"device","start":556530,"end":556810,"confidence":1,"speaker":"B"},{"text":"and","start":556810,"end":557010,"confidence":0.90478516,"speaker":"B"},{"text":"I","start":557010,"end":557210,"confidence":1,"speaker":"B"},{"text":"don't","start":557210,"end":557490,"confidence":0.98828125,"speaker":"B"},{"text":"think","start":557490,"end":557689,"confidence":1,"speaker":"B"},{"text":"that's","start":557689,"end":558010,"confidence":1,"speaker":"B"},{"text":"possible","start":558010,"end":558290,"confidence":1,"speaker":"B"},{"text":"with","start":558290,"end":558570,"confidence":0.9995117,"speaker":"B"},{"text":"Riverside.","start":558570,"end":559250,"confidence":0.9998372,"speaker":"B"}]},{"text":"Surprised? I mean, I know they have an app. Maybe he's using. I'm not sure if he's using. Using the app or not.","start":563250,"end":570070,"confidence":0.9345703,"words":[{"text":"Surprised?","start":563250,"end":563890,"confidence":0.9345703,"speaker":"A"},{"text":"I","start":564690,"end":564970,"confidence":0.9897461,"speaker":"A"},{"text":"mean,","start":564970,"end":565090,"confidence":0.99902344,"speaker":"A"},{"text":"I","start":565090,"end":565210,"confidence":0.99902344,"speaker":"A"},{"text":"know","start":565210,"end":565370,"confidence":1,"speaker":"A"},{"text":"they","start":565370,"end":565530,"confidence":1,"speaker":"A"},{"text":"have","start":565530,"end":565690,"confidence":1,"speaker":"A"},{"text":"an","start":565690,"end":565850,"confidence":0.99902344,"speaker":"A"},{"text":"app.","start":565850,"end":566130,"confidence":0.9863281,"speaker":"A"},{"text":"Maybe","start":567590,"end":567790,"confidence":0.93359375,"speaker":"B"},{"text":"he's","start":567790,"end":567990,"confidence":0.9996745,"speaker":"B"},{"text":"using.","start":567990,"end":568190,"confidence":0.99902344,"speaker":"B"},{"text":"I'm","start":568190,"end":568430,"confidence":0.99934894,"speaker":"B"},{"text":"not","start":568430,"end":568510,"confidence":0.99902344,"speaker":"B"},{"text":"sure","start":568510,"end":568630,"confidence":1,"speaker":"B"},{"text":"if","start":568630,"end":568710,"confidence":0.9980469,"speaker":"B"},{"text":"he's","start":568710,"end":568790,"confidence":0.9189453,"speaker":"B"},{"text":"using.","start":568790,"end":569030,"confidence":0.98535156,"speaker":"B"},{"text":"Using","start":569110,"end":569430,"confidence":1,"speaker":"B"},{"text":"the","start":569430,"end":569630,"confidence":0.99902344,"speaker":"B"},{"text":"app","start":569630,"end":569790,"confidence":0.9995117,"speaker":"B"},{"text":"or","start":569790,"end":569910,"confidence":0.9995117,"speaker":"B"},{"text":"not.","start":569910,"end":570070,"confidence":0.9995117,"speaker":"B"}]},{"text":"Okay.","start":570070,"end":570550,"confidence":0.99820966,"words":[{"text":"Okay.","start":570070,"end":570550,"confidence":0.99820966,"speaker":"A"}]},{"text":"Should I just go? Sure. Okay. Well, thanks for joining me, Evan. I really appreciate it.","start":575190,"end":585270,"confidence":0.99658203,"words":[{"text":"Should","start":575190,"end":575470,"confidence":0.99658203,"speaker":"A"},{"text":"I","start":575470,"end":575630,"confidence":0.8354492,"speaker":"A"},{"text":"just","start":575630,"end":575910,"confidence":1,"speaker":"A"},{"text":"go?","start":575910,"end":576310,"confidence":1,"speaker":"A"},{"text":"Sure.","start":578230,"end":578630,"confidence":1,"speaker":"B"},{"text":"Okay.","start":579830,"end":580470,"confidence":0.91015625,"speaker":"A"},{"text":"Well,","start":582390,"end":582710,"confidence":0.9980469,"speaker":"A"},{"text":"thanks","start":582710,"end":583030,"confidence":0.9926758,"speaker":"A"},{"text":"for","start":583030,"end":583230,"confidence":1,"speaker":"A"},{"text":"joining","start":583230,"end":583549,"confidence":0.75911456,"speaker":"A"},{"text":"me,","start":583549,"end":583830,"confidence":0.99902344,"speaker":"A"},{"text":"Evan.","start":583830,"end":584310,"confidence":0.9511719,"speaker":"A"},{"text":"I","start":584310,"end":584510,"confidence":0.9995117,"speaker":"A"},{"text":"really","start":584510,"end":584670,"confidence":0.9995117,"speaker":"A"},{"text":"appreciate","start":584670,"end":584990,"confidence":0.9088135,"speaker":"A"},{"text":"it.","start":584990,"end":585270,"confidence":0.99853516,"speaker":"A"}]},{"text":"I would say no. I mean I do, seriously. So yeah, this is a kind of a dry run. I would say I'm about 60% done with this presentation about CloudKit on the server and we'll probably hop back and forth between Keynote and not Keynote, but yeah. So this is CloudKit as your backend from iOS to server side Swift.","start":587430,"end":616630,"confidence":0.8666992,"words":[{"text":"I","start":587430,"end":587670,"confidence":0.8666992,"speaker":"A"},{"text":"would","start":587670,"end":587790,"confidence":0.67871094,"speaker":"A"},{"text":"say","start":587790,"end":588070,"confidence":0.9448242,"speaker":"A"},{"text":"no.","start":588390,"end":588630,"confidence":0.9951172,"speaker":"A"},{"text":"I","start":588630,"end":588710,"confidence":0.9995117,"speaker":"A"},{"text":"mean","start":588710,"end":588830,"confidence":0.95947266,"speaker":"A"},{"text":"I","start":588830,"end":588990,"confidence":0.99902344,"speaker":"A"},{"text":"do,","start":588990,"end":589270,"confidence":1,"speaker":"A"},{"text":"seriously.","start":589270,"end":589910,"confidence":0.99934894,"speaker":"A"},{"text":"So","start":591830,"end":592110,"confidence":0.9995117,"speaker":"A"},{"text":"yeah,","start":592110,"end":592470,"confidence":1,"speaker":"A"},{"text":"this","start":592630,"end":592910,"confidence":0.99902344,"speaker":"A"},{"text":"is","start":592910,"end":593030,"confidence":0.79296875,"speaker":"A"},{"text":"a","start":593030,"end":593150,"confidence":0.6645508,"speaker":"A"},{"text":"kind","start":593150,"end":593310,"confidence":0.99853516,"speaker":"A"},{"text":"of","start":593310,"end":593430,"confidence":0.9995117,"speaker":"A"},{"text":"a","start":593430,"end":593550,"confidence":0.99609375,"speaker":"A"},{"text":"dry","start":593550,"end":593830,"confidence":0.8828125,"speaker":"A"},{"text":"run.","start":593830,"end":594150,"confidence":0.9970703,"speaker":"A"},{"text":"I","start":594710,"end":594830,"confidence":0.9941406,"speaker":"A"},{"text":"would","start":594830,"end":594950,"confidence":0.9980469,"speaker":"A"},{"text":"say","start":594950,"end":595070,"confidence":0.99560547,"speaker":"A"},{"text":"I'm","start":595070,"end":595270,"confidence":0.99869794,"speaker":"A"},{"text":"about","start":595270,"end":595470,"confidence":0.9995117,"speaker":"A"},{"text":"60%","start":595470,"end":596110,"confidence":0.92505,"speaker":"A"},{"text":"done","start":596110,"end":596350,"confidence":0.9995117,"speaker":"A"},{"text":"with","start":596350,"end":596510,"confidence":1,"speaker":"A"},{"text":"this","start":596510,"end":596710,"confidence":0.99853516,"speaker":"A"},{"text":"presentation","start":596710,"end":597350,"confidence":1,"speaker":"A"},{"text":"about","start":599270,"end":599670,"confidence":0.9975586,"speaker":"A"},{"text":"CloudKit","start":600310,"end":600990,"confidence":0.7687988,"speaker":"A"},{"text":"on","start":600990,"end":601150,"confidence":0.9980469,"speaker":"A"},{"text":"the","start":601150,"end":601310,"confidence":0.9946289,"speaker":"A"},{"text":"server","start":601310,"end":601750,"confidence":0.7963867,"speaker":"A"},{"text":"and","start":604070,"end":604470,"confidence":0.9892578,"speaker":"A"},{"text":"we'll","start":604870,"end":605230,"confidence":0.9514974,"speaker":"A"},{"text":"probably","start":605230,"end":605470,"confidence":1,"speaker":"A"},{"text":"hop","start":605470,"end":605710,"confidence":0.9946289,"speaker":"A"},{"text":"back","start":605710,"end":605950,"confidence":0.9995117,"speaker":"A"},{"text":"and","start":605950,"end":606110,"confidence":1,"speaker":"A"},{"text":"forth","start":606110,"end":606350,"confidence":1,"speaker":"A"},{"text":"between","start":606350,"end":606630,"confidence":1,"speaker":"A"},{"text":"Keynote","start":606630,"end":607230,"confidence":0.88049316,"speaker":"A"},{"text":"and","start":607230,"end":607390,"confidence":0.9975586,"speaker":"A"},{"text":"not","start":607390,"end":607590,"confidence":0.9458008,"speaker":"A"},{"text":"Keynote,","start":607590,"end":608310,"confidence":0.99328613,"speaker":"A"},{"text":"but","start":608870,"end":609270,"confidence":0.9941406,"speaker":"A"},{"text":"yeah.","start":609510,"end":609990,"confidence":0.9737956,"speaker":"A"},{"text":"So","start":611670,"end":611950,"confidence":0.9946289,"speaker":"A"},{"text":"this","start":611950,"end":612110,"confidence":0.9995117,"speaker":"A"},{"text":"is","start":612110,"end":612310,"confidence":0.9995117,"speaker":"A"},{"text":"CloudKit","start":612310,"end":612910,"confidence":0.92456055,"speaker":"A"},{"text":"as","start":612910,"end":613070,"confidence":0.9863281,"speaker":"A"},{"text":"your","start":613070,"end":613230,"confidence":0.94628906,"speaker":"A"},{"text":"backend","start":613230,"end":613750,"confidence":0.8310547,"speaker":"A"},{"text":"from","start":613910,"end":614310,"confidence":1,"speaker":"A"},{"text":"iOS","start":614310,"end":614870,"confidence":0.9992676,"speaker":"A"},{"text":"to","start":615030,"end":615390,"confidence":0.9941406,"speaker":"A"},{"text":"server","start":615390,"end":615830,"confidence":0.9873047,"speaker":"A"},{"text":"side","start":615830,"end":616070,"confidence":0.5727539,"speaker":"A"},{"text":"Swift.","start":616070,"end":616630,"confidence":0.9953613,"speaker":"A"}]},{"text":"So what is CloudKit? CloudKit is a service launched by Apple probably a decade ago to kind of give developers a built in back end for storing data for their apps. One of the biggest benefits is is how cheap it is to use for iOS developers.","start":627600,"end":649970,"confidence":0.9916992,"words":[{"text":"So","start":627600,"end":627840,"confidence":0.9916992,"speaker":"A"},{"text":"what","start":628160,"end":628480,"confidence":0.9995117,"speaker":"A"},{"text":"is","start":628480,"end":628720,"confidence":0.99902344,"speaker":"A"},{"text":"CloudKit?","start":628720,"end":629440,"confidence":0.88281,"speaker":"A"},{"text":"CloudKit","start":629600,"end":630320,"confidence":0.88281,"speaker":"A"},{"text":"is","start":630320,"end":630600,"confidence":0.9921875,"speaker":"A"},{"text":"a","start":630600,"end":630880,"confidence":0.99853516,"speaker":"A"},{"text":"service","start":630880,"end":631200,"confidence":0.9995117,"speaker":"A"},{"text":"launched","start":632240,"end":632680,"confidence":0.99731445,"speaker":"A"},{"text":"by","start":632680,"end":632840,"confidence":1,"speaker":"A"},{"text":"Apple","start":632840,"end":633360,"confidence":1,"speaker":"A"},{"text":"probably","start":633600,"end":634000,"confidence":0.99869794,"speaker":"A"},{"text":"a","start":634000,"end":634160,"confidence":0.9995117,"speaker":"A"},{"text":"decade","start":634160,"end":634520,"confidence":0.99975586,"speaker":"A"},{"text":"ago","start":634520,"end":634800,"confidence":0.9995117,"speaker":"A"},{"text":"to","start":635920,"end":636279,"confidence":0.9848633,"speaker":"A"},{"text":"kind","start":636279,"end":636520,"confidence":0.8803711,"speaker":"A"},{"text":"of","start":636520,"end":636800,"confidence":0.98828125,"speaker":"A"},{"text":"give","start":636960,"end":637360,"confidence":0.9995117,"speaker":"A"},{"text":"developers","start":638880,"end":639680,"confidence":0.9995117,"speaker":"A"},{"text":"a","start":639840,"end":640200,"confidence":0.99902344,"speaker":"A"},{"text":"built","start":640200,"end":640520,"confidence":0.9995117,"speaker":"A"},{"text":"in","start":640520,"end":640720,"confidence":0.99316406,"speaker":"A"},{"text":"back","start":640720,"end":641000,"confidence":0.9995117,"speaker":"A"},{"text":"end","start":641000,"end":641280,"confidence":0.58935547,"speaker":"A"},{"text":"for","start":641280,"end":641520,"confidence":0.99609375,"speaker":"A"},{"text":"storing","start":641520,"end":641960,"confidence":0.9946289,"speaker":"A"},{"text":"data","start":641960,"end":642240,"confidence":0.99902344,"speaker":"A"},{"text":"for","start":642640,"end":642920,"confidence":0.9995117,"speaker":"A"},{"text":"their","start":642920,"end":643160,"confidence":0.99853516,"speaker":"A"},{"text":"apps.","start":643160,"end":643680,"confidence":0.99902344,"speaker":"A"},{"text":"One","start":644480,"end":644760,"confidence":0.9995117,"speaker":"A"},{"text":"of","start":644760,"end":644880,"confidence":0.9995117,"speaker":"A"},{"text":"the","start":644880,"end":645000,"confidence":0.99853516,"speaker":"A"},{"text":"biggest","start":645000,"end":645360,"confidence":1,"speaker":"A"},{"text":"benefits","start":645360,"end":646000,"confidence":0.99902344,"speaker":"A"},{"text":"is","start":646080,"end":646300,"confidence":0.84765625,"speaker":"A"},{"text":"is","start":646450,"end":646690,"confidence":0.9736328,"speaker":"A"},{"text":"how","start":646690,"end":647090,"confidence":0.9995117,"speaker":"A"},{"text":"cheap","start":647090,"end":647450,"confidence":0.9998372,"speaker":"A"},{"text":"it","start":647450,"end":647610,"confidence":0.9980469,"speaker":"A"},{"text":"is","start":647610,"end":647890,"confidence":0.9980469,"speaker":"A"},{"text":"to","start":647970,"end":648250,"confidence":0.99853516,"speaker":"A"},{"text":"use","start":648250,"end":648490,"confidence":0.9970703,"speaker":"A"},{"text":"for","start":648490,"end":648810,"confidence":0.9995117,"speaker":"A"},{"text":"iOS","start":648810,"end":649290,"confidence":0.9992676,"speaker":"A"},{"text":"developers.","start":649290,"end":649970,"confidence":0.998291,"speaker":"A"}]},{"text":"So if you have built an app, you could just add CloudKit right here within the Xcode project and use the regular CloudKit API in Swift to go ahead and start using it in your app.","start":652450,"end":670850,"confidence":0.95751953,"words":[{"text":"So","start":652450,"end":652850,"confidence":0.95751953,"speaker":"A"},{"text":"if","start":653570,"end":653850,"confidence":0.99853516,"speaker":"A"},{"text":"you","start":653850,"end":654130,"confidence":1,"speaker":"A"},{"text":"have","start":654450,"end":654850,"confidence":0.99902344,"speaker":"A"},{"text":"built","start":655330,"end":655690,"confidence":0.99934894,"speaker":"A"},{"text":"an","start":655690,"end":655850,"confidence":0.99560547,"speaker":"A"},{"text":"app,","start":655850,"end":656130,"confidence":0.9995117,"speaker":"A"},{"text":"you","start":656290,"end":656570,"confidence":1,"speaker":"A"},{"text":"could","start":656570,"end":656730,"confidence":0.6508789,"speaker":"A"},{"text":"just","start":656730,"end":656930,"confidence":0.99902344,"speaker":"A"},{"text":"add","start":656930,"end":657250,"confidence":0.99853516,"speaker":"A"},{"text":"CloudKit","start":657410,"end":658290,"confidence":0.89294,"speaker":"A"},{"text":"right","start":658290,"end":658610,"confidence":0.99853516,"speaker":"A"},{"text":"here","start":658610,"end":658930,"confidence":0.9995117,"speaker":"A"},{"text":"within","start":659570,"end":659970,"confidence":0.9975586,"speaker":"A"},{"text":"the","start":661330,"end":661730,"confidence":0.9970703,"speaker":"A"},{"text":"Xcode","start":662209,"end":662770,"confidence":0.91137695,"speaker":"A"},{"text":"project","start":662770,"end":663090,"confidence":1,"speaker":"A"},{"text":"and","start":663490,"end":663890,"confidence":0.9975586,"speaker":"A"},{"text":"use","start":665330,"end":665690,"confidence":0.99853516,"speaker":"A"},{"text":"the","start":665690,"end":665970,"confidence":0.9995117,"speaker":"A"},{"text":"regular","start":665970,"end":666370,"confidence":0.9995117,"speaker":"A"},{"text":"CloudKit","start":666370,"end":666970,"confidence":0.9975586,"speaker":"A"},{"text":"API","start":666970,"end":667490,"confidence":0.99853516,"speaker":"A"},{"text":"in","start":667890,"end":668170,"confidence":0.5913086,"speaker":"A"},{"text":"Swift","start":668170,"end":668570,"confidence":0.9951172,"speaker":"A"},{"text":"to","start":668570,"end":668810,"confidence":0.99902344,"speaker":"A"},{"text":"go","start":668810,"end":668970,"confidence":0.9975586,"speaker":"A"},{"text":"ahead","start":668970,"end":669250,"confidence":0.9765625,"speaker":"A"},{"text":"and","start":669250,"end":669530,"confidence":0.99902344,"speaker":"A"},{"text":"start","start":669530,"end":669730,"confidence":1,"speaker":"A"},{"text":"using","start":669730,"end":669930,"confidence":1,"speaker":"A"},{"text":"it","start":669930,"end":670130,"confidence":0.9980469,"speaker":"A"},{"text":"in","start":670130,"end":670330,"confidence":0.99902344,"speaker":"A"},{"text":"your","start":670330,"end":670530,"confidence":1,"speaker":"A"},{"text":"app.","start":670530,"end":670850,"confidence":0.9975586,"speaker":"A"}]},{"text":"Here is what it looks like to create a new record type. You can do all this through the CloudKit dashboard.","start":673390,"end":680190,"confidence":0.9946289,"words":[{"text":"Here","start":673390,"end":673630,"confidence":0.9946289,"speaker":"A"},{"text":"is","start":673630,"end":674030,"confidence":0.9995117,"speaker":"A"},{"text":"what","start":674030,"end":674430,"confidence":0.9995117,"speaker":"A"},{"text":"it","start":674430,"end":674750,"confidence":0.9980469,"speaker":"A"},{"text":"looks","start":674750,"end":675110,"confidence":1,"speaker":"A"},{"text":"like","start":675110,"end":675390,"confidence":0.99902344,"speaker":"A"},{"text":"to","start":675390,"end":675750,"confidence":0.99902344,"speaker":"A"},{"text":"create","start":675750,"end":675990,"confidence":0.9995117,"speaker":"A"},{"text":"a","start":675990,"end":676110,"confidence":0.9868164,"speaker":"A"},{"text":"new","start":676110,"end":676270,"confidence":0.99853516,"speaker":"A"},{"text":"record","start":676270,"end":676590,"confidence":0.9995117,"speaker":"A"},{"text":"type.","start":676590,"end":676990,"confidence":0.99194336,"speaker":"A"},{"text":"You","start":676990,"end":677150,"confidence":0.9995117,"speaker":"A"},{"text":"can","start":677150,"end":677270,"confidence":1,"speaker":"A"},{"text":"do","start":677270,"end":677430,"confidence":1,"speaker":"A"},{"text":"all","start":677430,"end":677590,"confidence":0.9995117,"speaker":"A"},{"text":"this","start":677590,"end":677870,"confidence":0.99853516,"speaker":"A"},{"text":"through","start":677870,"end":678270,"confidence":1,"speaker":"A"},{"text":"the","start":678430,"end":678790,"confidence":0.99902344,"speaker":"A"},{"text":"CloudKit","start":678790,"end":679510,"confidence":0.9987793,"speaker":"A"},{"text":"dashboard.","start":679510,"end":680190,"confidence":0.99938965,"speaker":"A"}]},{"text":"In CloudKit you could also do this using a schema file too. And you can export and import your schema that way. And it's not a SQL based database, it's much more, no sequel ish or an abstract layer above it. But essentially you can create records kind of like a table but not quite in your records. You can create a struct for it.","start":684190,"end":712680,"confidence":0.7402344,"words":[{"text":"In","start":684190,"end":684470,"confidence":0.7402344,"speaker":"A"},{"text":"CloudKit","start":684470,"end":685150,"confidence":0.9477539,"speaker":"A"},{"text":"you","start":685390,"end":685670,"confidence":0.9995117,"speaker":"A"},{"text":"could","start":685670,"end":685830,"confidence":0.8930664,"speaker":"A"},{"text":"also","start":685830,"end":686030,"confidence":0.9995117,"speaker":"A"},{"text":"do","start":686030,"end":686230,"confidence":1,"speaker":"A"},{"text":"this","start":686230,"end":686470,"confidence":1,"speaker":"A"},{"text":"using","start":686470,"end":686830,"confidence":1,"speaker":"A"},{"text":"a","start":687150,"end":687430,"confidence":0.94921875,"speaker":"A"},{"text":"schema","start":687430,"end":687910,"confidence":0.9895833,"speaker":"A"},{"text":"file","start":687910,"end":688270,"confidence":0.8520508,"speaker":"A"},{"text":"too.","start":688670,"end":689070,"confidence":0.8598633,"speaker":"A"},{"text":"And","start":689390,"end":689670,"confidence":0.99316406,"speaker":"A"},{"text":"you","start":689670,"end":689830,"confidence":0.98583984,"speaker":"A"},{"text":"can","start":689830,"end":689990,"confidence":0.6220703,"speaker":"A"},{"text":"export","start":689990,"end":690310,"confidence":0.9975586,"speaker":"A"},{"text":"and","start":690310,"end":690470,"confidence":0.9692383,"speaker":"A"},{"text":"import","start":690470,"end":690750,"confidence":0.9970703,"speaker":"A"},{"text":"your","start":690830,"end":691150,"confidence":0.99902344,"speaker":"A"},{"text":"schema","start":691150,"end":691710,"confidence":0.92041016,"speaker":"A"},{"text":"that","start":691710,"end":692030,"confidence":0.99658203,"speaker":"A"},{"text":"way.","start":692030,"end":692350,"confidence":0.9975586,"speaker":"A"},{"text":"And","start":693230,"end":693630,"confidence":0.98046875,"speaker":"A"},{"text":"it's","start":693630,"end":694070,"confidence":0.9996745,"speaker":"A"},{"text":"not","start":694070,"end":694350,"confidence":0.9980469,"speaker":"A"},{"text":"a","start":694590,"end":694870,"confidence":0.9321289,"speaker":"A"},{"text":"SQL","start":694870,"end":695190,"confidence":0.9423828,"speaker":"A"},{"text":"based","start":695190,"end":695430,"confidence":0.99902344,"speaker":"A"},{"text":"database,","start":695430,"end":696030,"confidence":0.9995117,"speaker":"A"},{"text":"it's","start":696030,"end":696270,"confidence":0.97802734,"speaker":"A"},{"text":"much","start":696270,"end":696470,"confidence":0.9980469,"speaker":"A"},{"text":"more,","start":696470,"end":696830,"confidence":0.9892578,"speaker":"A"},{"text":"no","start":697310,"end":697670,"confidence":0.9902344,"speaker":"A"},{"text":"sequel","start":697670,"end":698110,"confidence":0.8517253,"speaker":"A"},{"text":"ish","start":698110,"end":698430,"confidence":0.9033203,"speaker":"A"},{"text":"or","start":698430,"end":698630,"confidence":0.9995117,"speaker":"A"},{"text":"an","start":698630,"end":698830,"confidence":0.9770508,"speaker":"A"},{"text":"abstract","start":698830,"end":699350,"confidence":0.9822591,"speaker":"A"},{"text":"layer","start":699350,"end":699910,"confidence":0.99886066,"speaker":"A"},{"text":"above","start":699910,"end":700230,"confidence":0.98461914,"speaker":"A"},{"text":"it.","start":700230,"end":700510,"confidence":0.99609375,"speaker":"A"},{"text":"But","start":701400,"end":701560,"confidence":0.99658203,"speaker":"A"},{"text":"essentially","start":701560,"end":702240,"confidence":0.97021484,"speaker":"A"},{"text":"you","start":702240,"end":702600,"confidence":0.9995117,"speaker":"A"},{"text":"can","start":702680,"end":703080,"confidence":0.9995117,"speaker":"A"},{"text":"create","start":703080,"end":703440,"confidence":0.9970703,"speaker":"A"},{"text":"records","start":703440,"end":704120,"confidence":0.99658203,"speaker":"A"},{"text":"kind","start":704520,"end":704800,"confidence":0.99658203,"speaker":"A"},{"text":"of","start":704800,"end":704920,"confidence":0.9970703,"speaker":"A"},{"text":"like","start":704920,"end":705040,"confidence":0.9995117,"speaker":"A"},{"text":"a","start":705040,"end":705200,"confidence":0.9995117,"speaker":"A"},{"text":"table","start":705200,"end":705480,"confidence":0.9995117,"speaker":"A"},{"text":"but","start":705480,"end":705680,"confidence":0.99902344,"speaker":"A"},{"text":"not","start":705680,"end":705880,"confidence":0.99853516,"speaker":"A"},{"text":"quite","start":705880,"end":706280,"confidence":0.99853516,"speaker":"A"},{"text":"in","start":707000,"end":707280,"confidence":0.98339844,"speaker":"A"},{"text":"your","start":707280,"end":707520,"confidence":0.9970703,"speaker":"A"},{"text":"records.","start":707520,"end":708200,"confidence":0.9963379,"speaker":"A"},{"text":"You","start":709400,"end":709680,"confidence":0.99902344,"speaker":"A"},{"text":"can","start":709680,"end":709960,"confidence":0.9995117,"speaker":"A"},{"text":"create","start":710360,"end":710760,"confidence":0.9824219,"speaker":"A"},{"text":"a","start":711400,"end":711760,"confidence":0.9980469,"speaker":"A"},{"text":"struct","start":711760,"end":712240,"confidence":0.83862305,"speaker":"A"},{"text":"for","start":712240,"end":712480,"confidence":0.99902344,"speaker":"A"},{"text":"it.","start":712480,"end":712680,"confidence":0.9980469,"speaker":"A"}]},{"text":"You can just use CloudKit directly to go ahead and then you can then plug it into your app and do fun stuff like this. We can do things like queries and basic database stuff. There's a lot of advantages to it. For one, if you're doing Apple only, then it definitely makes sense to look into, at least look into CloudKit.","start":712680,"end":738080,"confidence":0.9995117,"words":[{"text":"You","start":712680,"end":712880,"confidence":0.9995117,"speaker":"A"},{"text":"can","start":712880,"end":713040,"confidence":0.9995117,"speaker":"A"},{"text":"just","start":713040,"end":713240,"confidence":1,"speaker":"A"},{"text":"use","start":713240,"end":713560,"confidence":0.99902344,"speaker":"A"},{"text":"CloudKit","start":713960,"end":714600,"confidence":0.982666,"speaker":"A"},{"text":"directly","start":714600,"end":715120,"confidence":0.9995117,"speaker":"A"},{"text":"to","start":715120,"end":715360,"confidence":0.9995117,"speaker":"A"},{"text":"go","start":715360,"end":715520,"confidence":0.9995117,"speaker":"A"},{"text":"ahead","start":715520,"end":715800,"confidence":0.99853516,"speaker":"A"},{"text":"and","start":716440,"end":716760,"confidence":0.9951172,"speaker":"A"},{"text":"then","start":716760,"end":717039,"confidence":0.99072266,"speaker":"A"},{"text":"you","start":717039,"end":717280,"confidence":0.98535156,"speaker":"A"},{"text":"can","start":717280,"end":717480,"confidence":0.88964844,"speaker":"A"},{"text":"then","start":717480,"end":717760,"confidence":0.78759766,"speaker":"A"},{"text":"plug","start":717760,"end":718080,"confidence":0.9995117,"speaker":"A"},{"text":"it","start":718080,"end":718240,"confidence":0.99902344,"speaker":"A"},{"text":"into","start":718240,"end":718440,"confidence":0.99902344,"speaker":"A"},{"text":"your","start":718440,"end":718680,"confidence":0.9995117,"speaker":"A"},{"text":"app","start":718680,"end":718920,"confidence":0.9995117,"speaker":"A"},{"text":"and","start":718920,"end":719240,"confidence":0.9628906,"speaker":"A"},{"text":"do","start":719240,"end":719520,"confidence":0.9995117,"speaker":"A"},{"text":"fun","start":719520,"end":719760,"confidence":0.99853516,"speaker":"A"},{"text":"stuff","start":719760,"end":720040,"confidence":1,"speaker":"A"},{"text":"like","start":720040,"end":720200,"confidence":0.9995117,"speaker":"A"},{"text":"this.","start":720200,"end":720520,"confidence":0.9946289,"speaker":"A"},{"text":"We","start":721560,"end":721880,"confidence":0.44580078,"speaker":"A"},{"text":"can","start":721880,"end":722080,"confidence":0.9995117,"speaker":"A"},{"text":"do","start":722080,"end":722240,"confidence":1,"speaker":"A"},{"text":"things","start":722240,"end":722440,"confidence":1,"speaker":"A"},{"text":"like","start":722440,"end":722760,"confidence":0.9995117,"speaker":"A"},{"text":"queries","start":722840,"end":723520,"confidence":0.9477539,"speaker":"A"},{"text":"and","start":723520,"end":723880,"confidence":0.8354492,"speaker":"A"},{"text":"basic","start":724840,"end":725280,"confidence":0.99975586,"speaker":"A"},{"text":"database","start":725280,"end":725800,"confidence":0.99869794,"speaker":"A"},{"text":"stuff.","start":725800,"end":726200,"confidence":0.9996745,"speaker":"A"},{"text":"There's","start":726200,"end":726640,"confidence":0.99153644,"speaker":"A"},{"text":"a","start":726640,"end":726760,"confidence":0.99902344,"speaker":"A"},{"text":"lot","start":726760,"end":726840,"confidence":1,"speaker":"A"},{"text":"of","start":726840,"end":726960,"confidence":0.99902344,"speaker":"A"},{"text":"advantages","start":726960,"end":727520,"confidence":0.9991862,"speaker":"A"},{"text":"to","start":727520,"end":727760,"confidence":0.99853516,"speaker":"A"},{"text":"it.","start":727760,"end":728040,"confidence":0.99658203,"speaker":"A"},{"text":"For","start":729280,"end":729440,"confidence":0.9794922,"speaker":"A"},{"text":"one,","start":729440,"end":729760,"confidence":0.9667969,"speaker":"A"},{"text":"if","start":730080,"end":730400,"confidence":0.9995117,"speaker":"A"},{"text":"you're","start":730400,"end":730880,"confidence":0.95996094,"speaker":"A"},{"text":"doing","start":730960,"end":731360,"confidence":0.99902344,"speaker":"A"},{"text":"Apple","start":731840,"end":732320,"confidence":1,"speaker":"A"},{"text":"only,","start":732320,"end":732640,"confidence":0.9995117,"speaker":"A"},{"text":"then","start":733600,"end":734000,"confidence":0.99658203,"speaker":"A"},{"text":"it","start":734000,"end":734280,"confidence":0.9995117,"speaker":"A"},{"text":"definitely","start":734280,"end":734680,"confidence":0.99938965,"speaker":"A"},{"text":"makes","start":734680,"end":734880,"confidence":0.9980469,"speaker":"A"},{"text":"sense","start":734880,"end":735280,"confidence":0.99975586,"speaker":"A"},{"text":"to","start":735520,"end":735840,"confidence":0.99853516,"speaker":"A"},{"text":"look","start":735840,"end":736120,"confidence":0.98046875,"speaker":"A"},{"text":"into,","start":736120,"end":736440,"confidence":0.53515625,"speaker":"A"},{"text":"at","start":736440,"end":736640,"confidence":0.9995117,"speaker":"A"},{"text":"least","start":736640,"end":736800,"confidence":0.9995117,"speaker":"A"},{"text":"look","start":736800,"end":737040,"confidence":0.99902344,"speaker":"A"},{"text":"into","start":737040,"end":737320,"confidence":0.99902344,"speaker":"A"},{"text":"CloudKit.","start":737320,"end":738080,"confidence":0.9995117,"speaker":"A"}]},{"text":"If you're just going to deploy to Apple Devices. If you don't mind the, the fact that it's not a regular SQL database, that's something too to think about. If you like need a SQL database, this might not be what you want. And then if you don't mind working with a lot of the abstraction layers that CloudKit provides, then this might be good for you to get started or especially if you don't have any database experience. So as far as like server choices, I would say CloudKit might not be your first choice, but it certainly is a decent choice if you're going the Apple only route.","start":742320,"end":784450,"confidence":0.9980469,"words":[{"text":"If","start":742320,"end":742600,"confidence":0.9980469,"speaker":"A"},{"text":"you're","start":742600,"end":742800,"confidence":0.9996745,"speaker":"A"},{"text":"just","start":742800,"end":742920,"confidence":0.9995117,"speaker":"A"},{"text":"going","start":742920,"end":743040,"confidence":0.92333984,"speaker":"A"},{"text":"to","start":743040,"end":743120,"confidence":0.99902344,"speaker":"A"},{"text":"deploy","start":743120,"end":743480,"confidence":1,"speaker":"A"},{"text":"to","start":743480,"end":743840,"confidence":0.99316406,"speaker":"A"},{"text":"Apple","start":744480,"end":744960,"confidence":0.99975586,"speaker":"A"},{"text":"Devices.","start":744960,"end":745440,"confidence":1,"speaker":"A"},{"text":"If","start":746080,"end":746440,"confidence":0.9995117,"speaker":"A"},{"text":"you","start":746440,"end":746800,"confidence":0.9995117,"speaker":"A"},{"text":"don't","start":747120,"end":747560,"confidence":0.9637044,"speaker":"A"},{"text":"mind","start":747560,"end":747920,"confidence":0.9995117,"speaker":"A"},{"text":"the,","start":748320,"end":748720,"confidence":0.9042969,"speaker":"A"},{"text":"the","start":749920,"end":750200,"confidence":0.9995117,"speaker":"A"},{"text":"fact","start":750200,"end":750360,"confidence":0.9995117,"speaker":"A"},{"text":"that","start":750360,"end":750520,"confidence":1,"speaker":"A"},{"text":"it's","start":750520,"end":750720,"confidence":0.9996745,"speaker":"A"},{"text":"not","start":750720,"end":750920,"confidence":0.84814453,"speaker":"A"},{"text":"a","start":750920,"end":751160,"confidence":0.5908203,"speaker":"A"},{"text":"regular","start":751160,"end":751560,"confidence":0.9992676,"speaker":"A"},{"text":"SQL","start":751560,"end":751960,"confidence":0.98860675,"speaker":"A"},{"text":"database,","start":751960,"end":752640,"confidence":0.9998372,"speaker":"A"},{"text":"that's","start":754050,"end":754210,"confidence":0.9980469,"speaker":"A"},{"text":"something","start":754210,"end":754410,"confidence":0.9995117,"speaker":"A"},{"text":"too","start":754410,"end":754650,"confidence":0.68408203,"speaker":"A"},{"text":"to","start":754650,"end":754810,"confidence":0.99853516,"speaker":"A"},{"text":"think","start":754810,"end":754930,"confidence":1,"speaker":"A"},{"text":"about.","start":754930,"end":755090,"confidence":0.9995117,"speaker":"A"},{"text":"If","start":755090,"end":755290,"confidence":0.9995117,"speaker":"A"},{"text":"you","start":755290,"end":755450,"confidence":1,"speaker":"A"},{"text":"like","start":755450,"end":755610,"confidence":0.92333984,"speaker":"A"},{"text":"need","start":755610,"end":755770,"confidence":0.9848633,"speaker":"A"},{"text":"a","start":755770,"end":755890,"confidence":0.9926758,"speaker":"A"},{"text":"SQL","start":755890,"end":756210,"confidence":0.96533203,"speaker":"A"},{"text":"database,","start":756210,"end":756650,"confidence":0.98063153,"speaker":"A"},{"text":"this","start":756650,"end":756850,"confidence":0.97998047,"speaker":"A"},{"text":"might","start":756850,"end":757050,"confidence":1,"speaker":"A"},{"text":"not","start":757050,"end":757210,"confidence":0.9995117,"speaker":"A"},{"text":"be","start":757210,"end":757490,"confidence":1,"speaker":"A"},{"text":"what","start":757730,"end":758050,"confidence":0.9819336,"speaker":"A"},{"text":"you","start":758050,"end":758370,"confidence":0.9995117,"speaker":"A"},{"text":"want.","start":758370,"end":758770,"confidence":0.9926758,"speaker":"A"},{"text":"And","start":759410,"end":759690,"confidence":0.95654297,"speaker":"A"},{"text":"then","start":759690,"end":759890,"confidence":0.9819336,"speaker":"A"},{"text":"if","start":759890,"end":760050,"confidence":1,"speaker":"A"},{"text":"you","start":760050,"end":760170,"confidence":1,"speaker":"A"},{"text":"don't","start":760170,"end":760370,"confidence":1,"speaker":"A"},{"text":"mind","start":760370,"end":760530,"confidence":1,"speaker":"A"},{"text":"working","start":760530,"end":760770,"confidence":1,"speaker":"A"},{"text":"with","start":760770,"end":761010,"confidence":0.9848633,"speaker":"A"},{"text":"a","start":761010,"end":761170,"confidence":0.99902344,"speaker":"A"},{"text":"lot","start":761170,"end":761290,"confidence":1,"speaker":"A"},{"text":"of","start":761290,"end":761410,"confidence":0.9995117,"speaker":"A"},{"text":"the","start":761410,"end":761530,"confidence":0.9995117,"speaker":"A"},{"text":"abstraction","start":761530,"end":762130,"confidence":0.9991455,"speaker":"A"},{"text":"layers","start":762130,"end":762610,"confidence":0.99934894,"speaker":"A"},{"text":"that","start":763010,"end":763330,"confidence":0.99853516,"speaker":"A"},{"text":"CloudKit","start":763330,"end":763970,"confidence":0.99902344,"speaker":"A"},{"text":"provides,","start":763970,"end":764610,"confidence":0.9995117,"speaker":"A"},{"text":"then","start":766930,"end":767330,"confidence":0.99658203,"speaker":"A"},{"text":"this","start":767650,"end":767970,"confidence":0.9995117,"speaker":"A"},{"text":"might","start":767970,"end":768170,"confidence":0.99609375,"speaker":"A"},{"text":"be","start":768170,"end":768370,"confidence":1,"speaker":"A"},{"text":"good","start":768370,"end":768530,"confidence":1,"speaker":"A"},{"text":"for","start":768530,"end":768650,"confidence":0.87402344,"speaker":"A"},{"text":"you","start":768650,"end":768850,"confidence":1,"speaker":"A"},{"text":"to","start":768850,"end":769050,"confidence":1,"speaker":"A"},{"text":"get","start":769050,"end":769210,"confidence":1,"speaker":"A"},{"text":"started","start":769210,"end":769490,"confidence":0.9995117,"speaker":"A"},{"text":"or","start":770050,"end":770410,"confidence":0.99658203,"speaker":"A"},{"text":"especially","start":770410,"end":770730,"confidence":0.9995117,"speaker":"A"},{"text":"if","start":770730,"end":770930,"confidence":1,"speaker":"A"},{"text":"you","start":770930,"end":771050,"confidence":1,"speaker":"A"},{"text":"don't","start":771050,"end":771250,"confidence":0.9995117,"speaker":"A"},{"text":"have","start":771250,"end":771370,"confidence":1,"speaker":"A"},{"text":"any","start":771370,"end":771570,"confidence":0.9995117,"speaker":"A"},{"text":"database","start":771570,"end":772130,"confidence":0.9998372,"speaker":"A"},{"text":"experience.","start":772130,"end":772450,"confidence":0.9995117,"speaker":"A"},{"text":"So","start":774130,"end":774410,"confidence":0.99316406,"speaker":"A"},{"text":"as","start":774410,"end":774570,"confidence":0.9995117,"speaker":"A"},{"text":"far","start":774570,"end":774730,"confidence":1,"speaker":"A"},{"text":"as","start":774730,"end":774930,"confidence":1,"speaker":"A"},{"text":"like","start":774930,"end":775250,"confidence":0.9770508,"speaker":"A"},{"text":"server","start":775570,"end":776090,"confidence":0.99975586,"speaker":"A"},{"text":"choices,","start":776090,"end":776650,"confidence":0.98291016,"speaker":"A"},{"text":"I","start":776650,"end":776850,"confidence":0.9995117,"speaker":"A"},{"text":"would","start":776850,"end":777010,"confidence":1,"speaker":"A"},{"text":"say","start":777010,"end":777290,"confidence":1,"speaker":"A"},{"text":"CloudKit","start":777290,"end":777970,"confidence":0.9926758,"speaker":"A"},{"text":"might","start":777970,"end":778170,"confidence":0.99365234,"speaker":"A"},{"text":"not","start":778170,"end":778330,"confidence":0.57714844,"speaker":"A"},{"text":"be","start":778330,"end":778490,"confidence":1,"speaker":"A"},{"text":"your","start":778490,"end":778690,"confidence":1,"speaker":"A"},{"text":"first","start":778690,"end":778930,"confidence":0.9995117,"speaker":"A"},{"text":"choice,","start":778930,"end":779330,"confidence":0.99975586,"speaker":"A"},{"text":"but","start":779970,"end":780090,"confidence":0.9970703,"speaker":"A"},{"text":"it","start":780090,"end":780250,"confidence":0.99902344,"speaker":"A"},{"text":"certainly","start":780250,"end":780610,"confidence":1,"speaker":"A"},{"text":"is","start":780610,"end":780930,"confidence":1,"speaker":"A"},{"text":"a","start":780930,"end":781210,"confidence":0.9995117,"speaker":"A"},{"text":"decent","start":781210,"end":781570,"confidence":1,"speaker":"A"},{"text":"choice","start":781570,"end":781970,"confidence":0.99975586,"speaker":"A"},{"text":"if","start":782290,"end":782610,"confidence":0.6225586,"speaker":"A"},{"text":"you're","start":782610,"end":782890,"confidence":0.9943034,"speaker":"A"},{"text":"going","start":782890,"end":783090,"confidence":0.99853516,"speaker":"A"},{"text":"the","start":783090,"end":783290,"confidence":0.9145508,"speaker":"A"},{"text":"Apple","start":783290,"end":783650,"confidence":0.9995117,"speaker":"A"},{"text":"only","start":783650,"end":783970,"confidence":0.9995117,"speaker":"A"},{"text":"route.","start":783970,"end":784450,"confidence":0.9938965,"speaker":"A"}]},{"text":"But then the question comes in, why would you want Cloud server side CloudKit? Why would you want to do anything with CloudKit on the server? So here's, here's the first case. Well, this is how you can go ahead and do that is they provide actually a REST API for calls to CloudKit using the, if you go to the documentation, I'll provide a link to that CloudKit Web Services which provides a lot of the documentation for what we'll be talking about today. A lot of this is abstracted out in the JavaScript library.","start":789970,"end":823790,"confidence":0.99658203,"words":[{"text":"But","start":789970,"end":790250,"confidence":0.99658203,"speaker":"A"},{"text":"then","start":790250,"end":790410,"confidence":1,"speaker":"A"},{"text":"the","start":790410,"end":790530,"confidence":1,"speaker":"A"},{"text":"question","start":790530,"end":790730,"confidence":1,"speaker":"A"},{"text":"comes","start":790730,"end":791010,"confidence":0.9951172,"speaker":"A"},{"text":"in,","start":791010,"end":791250,"confidence":0.97216797,"speaker":"A"},{"text":"why","start":791250,"end":791450,"confidence":1,"speaker":"A"},{"text":"would","start":791450,"end":791610,"confidence":1,"speaker":"A"},{"text":"you","start":791610,"end":791770,"confidence":1,"speaker":"A"},{"text":"want","start":791770,"end":792010,"confidence":0.99902344,"speaker":"A"},{"text":"Cloud","start":792010,"end":792450,"confidence":0.954834,"speaker":"A"},{"text":"server","start":792450,"end":792850,"confidence":0.98461914,"speaker":"A"},{"text":"side","start":792850,"end":793050,"confidence":0.55859375,"speaker":"A"},{"text":"CloudKit?","start":793050,"end":793730,"confidence":0.98095703,"speaker":"A"},{"text":"Why","start":793890,"end":794170,"confidence":1,"speaker":"A"},{"text":"would","start":794170,"end":794330,"confidence":1,"speaker":"A"},{"text":"you","start":794330,"end":794490,"confidence":1,"speaker":"A"},{"text":"want","start":794490,"end":794610,"confidence":0.9941406,"speaker":"A"},{"text":"to","start":794610,"end":794690,"confidence":0.9995117,"speaker":"A"},{"text":"do","start":794690,"end":794810,"confidence":1,"speaker":"A"},{"text":"anything","start":794810,"end":795090,"confidence":1,"speaker":"A"},{"text":"with","start":795090,"end":795250,"confidence":0.9995117,"speaker":"A"},{"text":"CloudKit","start":795250,"end":795810,"confidence":0.9885254,"speaker":"A"},{"text":"on","start":795810,"end":796009,"confidence":0.9980469,"speaker":"A"},{"text":"the","start":796009,"end":796170,"confidence":0.9995117,"speaker":"A"},{"text":"server?","start":796170,"end":796610,"confidence":1,"speaker":"A"},{"text":"So","start":797970,"end":798250,"confidence":0.99316406,"speaker":"A"},{"text":"here's,","start":798250,"end":798610,"confidence":0.9793294,"speaker":"A"},{"text":"here's","start":798610,"end":799090,"confidence":0.9996745,"speaker":"A"},{"text":"the","start":799250,"end":799530,"confidence":0.9995117,"speaker":"A"},{"text":"first","start":799530,"end":799810,"confidence":0.9995117,"speaker":"A"},{"text":"case.","start":799890,"end":800290,"confidence":0.9995117,"speaker":"A"},{"text":"Well,","start":800690,"end":801090,"confidence":0.96533203,"speaker":"A"},{"text":"this","start":801250,"end":801530,"confidence":0.9995117,"speaker":"A"},{"text":"is","start":801530,"end":801690,"confidence":1,"speaker":"A"},{"text":"how","start":801690,"end":801890,"confidence":1,"speaker":"A"},{"text":"you","start":801890,"end":802090,"confidence":1,"speaker":"A"},{"text":"can","start":802090,"end":802290,"confidence":0.9995117,"speaker":"A"},{"text":"go","start":802290,"end":802490,"confidence":0.9995117,"speaker":"A"},{"text":"ahead","start":802490,"end":802650,"confidence":0.9995117,"speaker":"A"},{"text":"and","start":802650,"end":802850,"confidence":0.97216797,"speaker":"A"},{"text":"do","start":802850,"end":803050,"confidence":1,"speaker":"A"},{"text":"that","start":803050,"end":803250,"confidence":0.99902344,"speaker":"A"},{"text":"is","start":803250,"end":803570,"confidence":0.90234375,"speaker":"A"},{"text":"they","start":803970,"end":804330,"confidence":0.99902344,"speaker":"A"},{"text":"provide","start":804330,"end":804690,"confidence":1,"speaker":"A"},{"text":"actually","start":804690,"end":805050,"confidence":0.9980469,"speaker":"A"},{"text":"a","start":805050,"end":805290,"confidence":0.91259766,"speaker":"A"},{"text":"REST","start":805290,"end":805490,"confidence":0.9995117,"speaker":"A"},{"text":"API","start":805490,"end":806090,"confidence":0.95166016,"speaker":"A"},{"text":"for","start":806090,"end":806450,"confidence":0.9946289,"speaker":"A"},{"text":"calls","start":806450,"end":806930,"confidence":0.9992676,"speaker":"A"},{"text":"to","start":806930,"end":807170,"confidence":0.9970703,"speaker":"A"},{"text":"CloudKit","start":807170,"end":807880,"confidence":0.9848633,"speaker":"A"},{"text":"using","start":808910,"end":809150,"confidence":0.95654297,"speaker":"A"},{"text":"the,","start":809310,"end":809710,"confidence":0.98828125,"speaker":"A"},{"text":"if","start":809950,"end":810230,"confidence":0.99902344,"speaker":"A"},{"text":"you","start":810230,"end":810350,"confidence":1,"speaker":"A"},{"text":"go","start":810350,"end":810430,"confidence":0.9995117,"speaker":"A"},{"text":"to","start":810430,"end":810550,"confidence":0.99853516,"speaker":"A"},{"text":"the","start":810550,"end":810670,"confidence":0.9995117,"speaker":"A"},{"text":"documentation,","start":810670,"end":811350,"confidence":0.99902344,"speaker":"A"},{"text":"I'll","start":811350,"end":811670,"confidence":0.99820966,"speaker":"A"},{"text":"provide","start":811670,"end":811910,"confidence":0.99658203,"speaker":"A"},{"text":"a","start":811910,"end":812110,"confidence":0.9067383,"speaker":"A"},{"text":"link","start":812110,"end":812350,"confidence":0.9992676,"speaker":"A"},{"text":"to","start":812350,"end":812550,"confidence":0.9995117,"speaker":"A"},{"text":"that","start":812550,"end":812830,"confidence":0.8276367,"speaker":"A"},{"text":"CloudKit","start":812910,"end":813590,"confidence":0.87280273,"speaker":"A"},{"text":"Web","start":813590,"end":813830,"confidence":0.99658203,"speaker":"A"},{"text":"Services","start":813830,"end":814110,"confidence":0.9995117,"speaker":"A"},{"text":"which","start":815310,"end":815710,"confidence":0.99902344,"speaker":"A"},{"text":"provides","start":816510,"end":816990,"confidence":0.99975586,"speaker":"A"},{"text":"a","start":816990,"end":817070,"confidence":0.9995117,"speaker":"A"},{"text":"lot","start":817070,"end":817190,"confidence":1,"speaker":"A"},{"text":"of","start":817190,"end":817310,"confidence":0.9995117,"speaker":"A"},{"text":"the","start":817310,"end":817430,"confidence":0.9980469,"speaker":"A"},{"text":"documentation","start":817430,"end":818070,"confidence":0.9998047,"speaker":"A"},{"text":"for","start":818070,"end":818270,"confidence":0.9995117,"speaker":"A"},{"text":"what","start":818270,"end":818390,"confidence":0.99902344,"speaker":"A"},{"text":"we'll","start":818390,"end":818630,"confidence":0.8699544,"speaker":"A"},{"text":"be","start":818630,"end":818790,"confidence":1,"speaker":"A"},{"text":"talking","start":818790,"end":819030,"confidence":0.97631836,"speaker":"A"},{"text":"about","start":819030,"end":819230,"confidence":0.9995117,"speaker":"A"},{"text":"today.","start":819230,"end":819550,"confidence":0.99902344,"speaker":"A"},{"text":"A","start":820910,"end":821150,"confidence":0.99658203,"speaker":"A"},{"text":"lot","start":821150,"end":821270,"confidence":1,"speaker":"A"},{"text":"of","start":821270,"end":821430,"confidence":0.9995117,"speaker":"A"},{"text":"this","start":821430,"end":821590,"confidence":0.99902344,"speaker":"A"},{"text":"is","start":821590,"end":821790,"confidence":0.99853516,"speaker":"A"},{"text":"abstracted","start":821790,"end":822390,"confidence":0.88964844,"speaker":"A"},{"text":"out","start":822390,"end":822550,"confidence":0.99902344,"speaker":"A"},{"text":"in","start":822550,"end":822670,"confidence":0.9980469,"speaker":"A"},{"text":"the","start":822670,"end":822750,"confidence":0.9995117,"speaker":"A"},{"text":"JavaScript","start":822750,"end":823350,"confidence":0.99698895,"speaker":"A"},{"text":"library.","start":823350,"end":823790,"confidence":0.9916992,"speaker":"A"}]},{"text":"So if you want to do stuff on a website, they provide a CloudKit JavaScript library for that. Sorry, just going into do not disturb mode.","start":823870,"end":839230,"confidence":0.9838867,"words":[{"text":"So","start":823870,"end":824109,"confidence":0.9838867,"speaker":"A"},{"text":"if","start":824109,"end":824230,"confidence":0.99902344,"speaker":"A"},{"text":"you","start":824230,"end":824350,"confidence":1,"speaker":"A"},{"text":"want","start":824350,"end":824510,"confidence":0.95166016,"speaker":"A"},{"text":"to","start":824510,"end":824670,"confidence":0.9980469,"speaker":"A"},{"text":"do","start":824670,"end":824790,"confidence":0.9995117,"speaker":"A"},{"text":"stuff","start":824790,"end":824990,"confidence":1,"speaker":"A"},{"text":"on","start":824990,"end":825110,"confidence":0.9995117,"speaker":"A"},{"text":"a","start":825110,"end":825270,"confidence":0.98828125,"speaker":"A"},{"text":"website,","start":825270,"end":825550,"confidence":0.99609375,"speaker":"A"},{"text":"they","start":826430,"end":826790,"confidence":0.9995117,"speaker":"A"},{"text":"provide","start":826790,"end":827150,"confidence":1,"speaker":"A"},{"text":"a","start":827230,"end":827630,"confidence":0.99853516,"speaker":"A"},{"text":"CloudKit","start":827790,"end":828590,"confidence":0.99438477,"speaker":"A"},{"text":"JavaScript","start":828590,"end":829390,"confidence":0.9239909,"speaker":"A"},{"text":"library","start":830270,"end":830830,"confidence":0.9996745,"speaker":"A"},{"text":"for","start":830830,"end":831110,"confidence":0.99853516,"speaker":"A"},{"text":"that.","start":831110,"end":831470,"confidence":0.99609375,"speaker":"A"},{"text":"Sorry,","start":833150,"end":833710,"confidence":0.8925781,"speaker":"A"},{"text":"just","start":836190,"end":836310,"confidence":0.93847656,"speaker":"A"},{"text":"going","start":836310,"end":836510,"confidence":0.9814453,"speaker":"A"},{"text":"into","start":836510,"end":836790,"confidence":0.9121094,"speaker":"A"},{"text":"do","start":836790,"end":837030,"confidence":0.99560547,"speaker":"A"},{"text":"not","start":837030,"end":837230,"confidence":0.99902344,"speaker":"A"},{"text":"disturb","start":837230,"end":837870,"confidence":0.87369794,"speaker":"A"},{"text":"mode.","start":838670,"end":839230,"confidence":0.73999023,"speaker":"A"}]},{"text":"They even in that web references documentation they provide a composing web service request and all these instructions about how to go ahead and do that. So man, was it like half a decade ago that I built Heart Twitch and at the time I don't think there was anything, there was anything like sign in with Apple even. And like I really didn't want like to explain how harshwitch works is you have like a watch and it will send the heart rate to the server and then the server will then use a web socket to push it out to a web page. And then you would point OBS or some sort of streaming software to the URL or to the browser window and then that way you can stream your heart rate. That's how it works.","start":847950,"end":900860,"confidence":0.9404297,"words":[{"text":"They","start":847950,"end":848270,"confidence":0.9404297,"speaker":"A"},{"text":"even","start":848270,"end":848590,"confidence":0.7373047,"speaker":"A"},{"text":"in","start":848750,"end":849030,"confidence":0.9995117,"speaker":"A"},{"text":"that","start":849030,"end":849270,"confidence":0.99902344,"speaker":"A"},{"text":"web","start":849270,"end":849710,"confidence":0.9995117,"speaker":"A"},{"text":"references","start":849790,"end":850429,"confidence":0.9367676,"speaker":"A"},{"text":"documentation","start":850430,"end":851070,"confidence":0.97734374,"speaker":"A"},{"text":"they","start":851070,"end":851270,"confidence":0.9980469,"speaker":"A"},{"text":"provide","start":851270,"end":851510,"confidence":1,"speaker":"A"},{"text":"a","start":851510,"end":851710,"confidence":0.8413086,"speaker":"A"},{"text":"composing","start":851710,"end":852150,"confidence":0.92008466,"speaker":"A"},{"text":"web","start":852150,"end":852390,"confidence":0.998291,"speaker":"A"},{"text":"service","start":852390,"end":852630,"confidence":0.99902344,"speaker":"A"},{"text":"request","start":852630,"end":853150,"confidence":0.9995117,"speaker":"A"},{"text":"and","start":853470,"end":853750,"confidence":0.9970703,"speaker":"A"},{"text":"all","start":853750,"end":853910,"confidence":0.9995117,"speaker":"A"},{"text":"these","start":853910,"end":854110,"confidence":0.99902344,"speaker":"A"},{"text":"instructions","start":854110,"end":854670,"confidence":0.9996745,"speaker":"A"},{"text":"about","start":854670,"end":854910,"confidence":1,"speaker":"A"},{"text":"how","start":854910,"end":855070,"confidence":0.9995117,"speaker":"A"},{"text":"to","start":855070,"end":855190,"confidence":1,"speaker":"A"},{"text":"go","start":855190,"end":855310,"confidence":1,"speaker":"A"},{"text":"ahead","start":855310,"end":855470,"confidence":0.9995117,"speaker":"A"},{"text":"and","start":855470,"end":855670,"confidence":1,"speaker":"A"},{"text":"do","start":855670,"end":855830,"confidence":1,"speaker":"A"},{"text":"that.","start":855830,"end":856110,"confidence":1,"speaker":"A"},{"text":"So","start":857470,"end":857870,"confidence":0.98876953,"speaker":"A"},{"text":"man,","start":858270,"end":858590,"confidence":0.9482422,"speaker":"A"},{"text":"was","start":858590,"end":858790,"confidence":0.99853516,"speaker":"A"},{"text":"it","start":858790,"end":858950,"confidence":0.9277344,"speaker":"A"},{"text":"like","start":858950,"end":859110,"confidence":0.9941406,"speaker":"A"},{"text":"half","start":859110,"end":859310,"confidence":0.9995117,"speaker":"A"},{"text":"a","start":859310,"end":859470,"confidence":0.99902344,"speaker":"A"},{"text":"decade","start":859470,"end":859790,"confidence":0.99975586,"speaker":"A"},{"text":"ago","start":859790,"end":860110,"confidence":1,"speaker":"A"},{"text":"that","start":860880,"end":861120,"confidence":0.97216797,"speaker":"A"},{"text":"I","start":861280,"end":861680,"confidence":0.97314453,"speaker":"A"},{"text":"built","start":862960,"end":863320,"confidence":0.99153644,"speaker":"A"},{"text":"Heart","start":863320,"end":863520,"confidence":0.8129883,"speaker":"A"},{"text":"Twitch","start":863520,"end":864000,"confidence":0.98999023,"speaker":"A"},{"text":"and","start":864480,"end":864880,"confidence":0.9814453,"speaker":"A"},{"text":"at","start":865360,"end":865640,"confidence":0.99902344,"speaker":"A"},{"text":"the","start":865640,"end":865840,"confidence":0.99853516,"speaker":"A"},{"text":"time","start":865840,"end":866080,"confidence":1,"speaker":"A"},{"text":"I","start":866080,"end":866280,"confidence":1,"speaker":"A"},{"text":"don't","start":866280,"end":866520,"confidence":0.99934894,"speaker":"A"},{"text":"think","start":866520,"end":866720,"confidence":1,"speaker":"A"},{"text":"there","start":866720,"end":866960,"confidence":0.99365234,"speaker":"A"},{"text":"was","start":866960,"end":867280,"confidence":0.9995117,"speaker":"A"},{"text":"anything,","start":867440,"end":868080,"confidence":0.99975586,"speaker":"A"},{"text":"there","start":870080,"end":870360,"confidence":0.99658203,"speaker":"A"},{"text":"was","start":870360,"end":870560,"confidence":0.99902344,"speaker":"A"},{"text":"anything","start":870560,"end":870960,"confidence":0.99975586,"speaker":"A"},{"text":"like","start":870960,"end":871200,"confidence":0.99902344,"speaker":"A"},{"text":"sign","start":871200,"end":871440,"confidence":0.99658203,"speaker":"A"},{"text":"in","start":871440,"end":871640,"confidence":0.9819336,"speaker":"A"},{"text":"with","start":871640,"end":871800,"confidence":1,"speaker":"A"},{"text":"Apple","start":871800,"end":872160,"confidence":0.9995117,"speaker":"A"},{"text":"even.","start":872160,"end":872480,"confidence":0.9970703,"speaker":"A"},{"text":"And","start":872880,"end":873280,"confidence":0.97265625,"speaker":"A"},{"text":"like","start":873520,"end":873840,"confidence":0.9399414,"speaker":"A"},{"text":"I","start":873840,"end":874160,"confidence":0.9995117,"speaker":"A"},{"text":"really","start":874160,"end":874560,"confidence":0.99902344,"speaker":"A"},{"text":"didn't","start":875120,"end":875640,"confidence":0.99348956,"speaker":"A"},{"text":"want","start":875640,"end":875920,"confidence":0.9995117,"speaker":"A"},{"text":"like","start":876880,"end":877280,"confidence":0.9794922,"speaker":"A"},{"text":"to","start":878160,"end":878480,"confidence":0.98291016,"speaker":"A"},{"text":"explain","start":878480,"end":878760,"confidence":0.99853516,"speaker":"A"},{"text":"how","start":878760,"end":878920,"confidence":0.9995117,"speaker":"A"},{"text":"harshwitch","start":878920,"end":879520,"confidence":0.62939453,"speaker":"A"},{"text":"works","start":879520,"end":879800,"confidence":0.99975586,"speaker":"A"},{"text":"is","start":879800,"end":879960,"confidence":0.91064453,"speaker":"A"},{"text":"you","start":879960,"end":880120,"confidence":0.99853516,"speaker":"A"},{"text":"have","start":880120,"end":880320,"confidence":1,"speaker":"A"},{"text":"like","start":880320,"end":880520,"confidence":0.9902344,"speaker":"A"},{"text":"a","start":880520,"end":880680,"confidence":0.9995117,"speaker":"A"},{"text":"watch","start":880680,"end":880960,"confidence":0.99902344,"speaker":"A"},{"text":"and","start":881360,"end":881720,"confidence":0.6225586,"speaker":"A"},{"text":"it","start":881720,"end":881960,"confidence":0.9995117,"speaker":"A"},{"text":"will","start":881960,"end":882200,"confidence":0.9995117,"speaker":"A"},{"text":"send","start":882200,"end":882600,"confidence":0.9291992,"speaker":"A"},{"text":"the","start":882600,"end":882840,"confidence":0.9995117,"speaker":"A"},{"text":"heart","start":882840,"end":883040,"confidence":0.9995117,"speaker":"A"},{"text":"rate","start":883040,"end":883280,"confidence":0.9995117,"speaker":"A"},{"text":"to","start":883280,"end":883480,"confidence":1,"speaker":"A"},{"text":"the","start":883480,"end":883640,"confidence":1,"speaker":"A"},{"text":"server","start":883640,"end":884160,"confidence":0.99975586,"speaker":"A"},{"text":"and","start":885360,"end":885640,"confidence":0.9921875,"speaker":"A"},{"text":"then","start":885640,"end":885920,"confidence":0.9926758,"speaker":"A"},{"text":"the","start":887020,"end":887180,"confidence":0.99658203,"speaker":"A"},{"text":"server","start":887180,"end":887580,"confidence":1,"speaker":"A"},{"text":"will","start":887580,"end":887780,"confidence":0.99902344,"speaker":"A"},{"text":"then","start":887780,"end":888020,"confidence":0.9995117,"speaker":"A"},{"text":"use","start":888020,"end":888260,"confidence":1,"speaker":"A"},{"text":"a","start":888260,"end":888420,"confidence":0.99853516,"speaker":"A"},{"text":"web","start":888420,"end":888660,"confidence":0.7871094,"speaker":"A"},{"text":"socket","start":888660,"end":889180,"confidence":0.9996745,"speaker":"A"},{"text":"to","start":889180,"end":889540,"confidence":0.9995117,"speaker":"A"},{"text":"push","start":889540,"end":889860,"confidence":1,"speaker":"A"},{"text":"it","start":889860,"end":890020,"confidence":0.99902344,"speaker":"A"},{"text":"out","start":890020,"end":890180,"confidence":0.9995117,"speaker":"A"},{"text":"to","start":890180,"end":890340,"confidence":1,"speaker":"A"},{"text":"a","start":890340,"end":890500,"confidence":0.99853516,"speaker":"A"},{"text":"web","start":890500,"end":890740,"confidence":0.99975586,"speaker":"A"},{"text":"page.","start":890740,"end":891100,"confidence":0.84643555,"speaker":"A"},{"text":"And","start":892060,"end":892340,"confidence":0.97558594,"speaker":"A"},{"text":"then","start":892340,"end":892620,"confidence":0.9995117,"speaker":"A"},{"text":"you","start":892620,"end":892900,"confidence":0.99902344,"speaker":"A"},{"text":"would","start":892900,"end":893180,"confidence":0.9838867,"speaker":"A"},{"text":"point","start":893500,"end":893900,"confidence":0.9926758,"speaker":"A"},{"text":"OBS","start":893980,"end":894380,"confidence":0.9897461,"speaker":"A"},{"text":"or","start":894540,"end":894780,"confidence":0.99072266,"speaker":"A"},{"text":"some","start":894780,"end":894900,"confidence":0.9995117,"speaker":"A"},{"text":"sort","start":894900,"end":895100,"confidence":0.9926758,"speaker":"A"},{"text":"of","start":895100,"end":895260,"confidence":0.53027344,"speaker":"A"},{"text":"streaming","start":895260,"end":895700,"confidence":0.91813153,"speaker":"A"},{"text":"software","start":895700,"end":896020,"confidence":0.9998779,"speaker":"A"},{"text":"to","start":896020,"end":896180,"confidence":0.9995117,"speaker":"A"},{"text":"the","start":896180,"end":896340,"confidence":1,"speaker":"A"},{"text":"URL","start":896340,"end":896860,"confidence":0.99487305,"speaker":"A"},{"text":"or","start":896860,"end":897060,"confidence":0.9980469,"speaker":"A"},{"text":"to","start":897060,"end":897220,"confidence":0.99609375,"speaker":"A"},{"text":"the","start":897220,"end":897340,"confidence":1,"speaker":"A"},{"text":"browser","start":897340,"end":897700,"confidence":0.9983724,"speaker":"A"},{"text":"window","start":897700,"end":898060,"confidence":1,"speaker":"A"},{"text":"and","start":898060,"end":898220,"confidence":0.99072266,"speaker":"A"},{"text":"then","start":898220,"end":898380,"confidence":0.8310547,"speaker":"A"},{"text":"that","start":898380,"end":898580,"confidence":0.9995117,"speaker":"A"},{"text":"way","start":898580,"end":898740,"confidence":0.9995117,"speaker":"A"},{"text":"you","start":898740,"end":898860,"confidence":1,"speaker":"A"},{"text":"can","start":898860,"end":898980,"confidence":0.9995117,"speaker":"A"},{"text":"stream","start":898980,"end":899260,"confidence":0.99609375,"speaker":"A"},{"text":"your","start":899260,"end":899460,"confidence":0.99853516,"speaker":"A"},{"text":"heart","start":899460,"end":899660,"confidence":0.9980469,"speaker":"A"},{"text":"rate.","start":899660,"end":899940,"confidence":0.9951172,"speaker":"A"},{"text":"That's","start":899940,"end":900220,"confidence":0.9996745,"speaker":"A"},{"text":"how","start":900220,"end":900300,"confidence":0.9995117,"speaker":"A"},{"text":"it","start":900300,"end":900420,"confidence":0.99853516,"speaker":"A"},{"text":"works.","start":900420,"end":900860,"confidence":0.9946289,"speaker":"A"}]},{"text":"And what I really didn't want is a difficult way for a user to log in with a username and password on the watch because we all know typing on the watch is hell. So my, my thought was like, and I didn't have sign in with Apple, right? So my thought was why don't we use CloudKit? Because you're already signed in a CloudKit on the Watch with your, your id.","start":901500,"end":924080,"confidence":0.9711914,"words":[{"text":"And","start":901500,"end":901780,"confidence":0.9711914,"speaker":"A"},{"text":"what","start":901780,"end":901940,"confidence":0.9995117,"speaker":"A"},{"text":"I","start":901940,"end":902100,"confidence":1,"speaker":"A"},{"text":"really","start":902100,"end":902339,"confidence":0.9995117,"speaker":"A"},{"text":"didn't","start":902339,"end":902659,"confidence":0.9980469,"speaker":"A"},{"text":"want","start":902659,"end":902900,"confidence":1,"speaker":"A"},{"text":"is","start":902900,"end":903180,"confidence":0.99902344,"speaker":"A"},{"text":"a","start":903180,"end":903500,"confidence":0.9711914,"speaker":"A"},{"text":"difficult","start":903500,"end":903980,"confidence":0.9699707,"speaker":"A"},{"text":"way","start":903980,"end":904180,"confidence":0.9995117,"speaker":"A"},{"text":"for","start":904180,"end":904380,"confidence":0.9995117,"speaker":"A"},{"text":"a","start":904380,"end":904580,"confidence":0.8876953,"speaker":"A"},{"text":"user","start":904580,"end":904900,"confidence":1,"speaker":"A"},{"text":"to","start":904900,"end":905100,"confidence":0.9995117,"speaker":"A"},{"text":"log","start":905100,"end":905420,"confidence":1,"speaker":"A"},{"text":"in","start":905420,"end":905820,"confidence":0.9838867,"speaker":"A"},{"text":"with","start":906540,"end":906820,"confidence":0.9995117,"speaker":"A"},{"text":"a","start":906820,"end":906980,"confidence":0.7949219,"speaker":"A"},{"text":"username","start":906980,"end":907500,"confidence":0.9975586,"speaker":"A"},{"text":"and","start":907500,"end":907620,"confidence":0.99902344,"speaker":"A"},{"text":"password","start":907620,"end":908020,"confidence":0.90152997,"speaker":"A"},{"text":"on","start":908020,"end":908180,"confidence":0.6225586,"speaker":"A"},{"text":"the","start":908180,"end":908340,"confidence":0.9995117,"speaker":"A"},{"text":"watch","start":908340,"end":908620,"confidence":0.9995117,"speaker":"A"},{"text":"because","start":908620,"end":908900,"confidence":0.72558594,"speaker":"A"},{"text":"we","start":908900,"end":909020,"confidence":0.9995117,"speaker":"A"},{"text":"all","start":909020,"end":909140,"confidence":0.99902344,"speaker":"A"},{"text":"know","start":909140,"end":909300,"confidence":0.9980469,"speaker":"A"},{"text":"typing","start":909300,"end":909620,"confidence":0.8249512,"speaker":"A"},{"text":"on","start":909620,"end":909740,"confidence":0.9921875,"speaker":"A"},{"text":"the","start":909740,"end":909820,"confidence":0.9951172,"speaker":"A"},{"text":"watch","start":909820,"end":910020,"confidence":0.99902344,"speaker":"A"},{"text":"is","start":910020,"end":910380,"confidence":0.84472656,"speaker":"A"},{"text":"hell.","start":910780,"end":911260,"confidence":0.9157715,"speaker":"A"},{"text":"So","start":911900,"end":912300,"confidence":0.9770508,"speaker":"A"},{"text":"my,","start":912460,"end":912860,"confidence":0.70410156,"speaker":"A"},{"text":"my","start":912860,"end":913140,"confidence":0.9995117,"speaker":"A"},{"text":"thought","start":913140,"end":913340,"confidence":0.99902344,"speaker":"A"},{"text":"was","start":913340,"end":913620,"confidence":0.99853516,"speaker":"A"},{"text":"like,","start":913620,"end":913980,"confidence":0.9897461,"speaker":"A"},{"text":"and","start":914320,"end":914480,"confidence":0.6791992,"speaker":"A"},{"text":"I","start":914480,"end":914680,"confidence":1,"speaker":"A"},{"text":"didn't","start":914680,"end":914920,"confidence":0.9996745,"speaker":"A"},{"text":"have","start":914920,"end":915200,"confidence":0.9921875,"speaker":"A"},{"text":"sign","start":915280,"end":915600,"confidence":0.8886719,"speaker":"A"},{"text":"in","start":915600,"end":915800,"confidence":0.59814453,"speaker":"A"},{"text":"with","start":915800,"end":915960,"confidence":1,"speaker":"A"},{"text":"Apple,","start":915960,"end":916280,"confidence":1,"speaker":"A"},{"text":"right?","start":916280,"end":916560,"confidence":0.9970703,"speaker":"A"},{"text":"So","start":917440,"end":917720,"confidence":0.9995117,"speaker":"A"},{"text":"my","start":917720,"end":917880,"confidence":0.99902344,"speaker":"A"},{"text":"thought","start":917880,"end":918080,"confidence":0.9995117,"speaker":"A"},{"text":"was","start":918080,"end":918320,"confidence":0.99902344,"speaker":"A"},{"text":"why","start":918320,"end":918520,"confidence":1,"speaker":"A"},{"text":"don't","start":918520,"end":918720,"confidence":0.9972331,"speaker":"A"},{"text":"we","start":918720,"end":918840,"confidence":1,"speaker":"A"},{"text":"use","start":918840,"end":919000,"confidence":1,"speaker":"A"},{"text":"CloudKit?","start":919000,"end":919680,"confidence":0.9992676,"speaker":"A"},{"text":"Because","start":919840,"end":920120,"confidence":0.98095703,"speaker":"A"},{"text":"you're","start":920120,"end":920320,"confidence":0.9998372,"speaker":"A"},{"text":"already","start":920320,"end":920520,"confidence":1,"speaker":"A"},{"text":"signed","start":920520,"end":920880,"confidence":0.9963379,"speaker":"A"},{"text":"in","start":920880,"end":921000,"confidence":0.71728516,"speaker":"A"},{"text":"a","start":921000,"end":921120,"confidence":0.61376953,"speaker":"A"},{"text":"CloudKit","start":921120,"end":921640,"confidence":0.99658203,"speaker":"A"},{"text":"on","start":921640,"end":921800,"confidence":0.9995117,"speaker":"A"},{"text":"the","start":921800,"end":921960,"confidence":1,"speaker":"A"},{"text":"Watch","start":921960,"end":922240,"confidence":0.99853516,"speaker":"A"},{"text":"with","start":922800,"end":923120,"confidence":0.99853516,"speaker":"A"},{"text":"your,","start":923120,"end":923440,"confidence":0.9980469,"speaker":"A"},{"text":"your","start":923440,"end":923760,"confidence":0.9995117,"speaker":"A"},{"text":"id.","start":923760,"end":924080,"confidence":0.9995117,"speaker":"A"}]},{"text":"And what you do is you log in with a regular like email address and password in Heart Twitch on the website. And then there's a little, there's a site, there's a part of the site where you can sign into CloudKit and then from there you can, because, because of the CloudKit JavaScript library, you can then I can then pull the all the devices because when you first launch the app on the Watch, it adds your watch to the CloudKit database. And then I could pull that in and then add that to my postgres database. So then there is no need for authentication because I already have the CloudKit, the device added in my postgres database. So it's kind of like knows, oh yeah, this is Leo's watch, he doesn't need to authenticate.","start":926640,"end":975520,"confidence":0.99316406,"words":[{"text":"And","start":926640,"end":926920,"confidence":0.99316406,"speaker":"A"},{"text":"what","start":926920,"end":927080,"confidence":0.99902344,"speaker":"A"},{"text":"you","start":927080,"end":927320,"confidence":1,"speaker":"A"},{"text":"do","start":927320,"end":927680,"confidence":1,"speaker":"A"},{"text":"is","start":928320,"end":928720,"confidence":0.99902344,"speaker":"A"},{"text":"you","start":929440,"end":929720,"confidence":0.9995117,"speaker":"A"},{"text":"log","start":929720,"end":929920,"confidence":1,"speaker":"A"},{"text":"in","start":929920,"end":930159,"confidence":0.9975586,"speaker":"A"},{"text":"with","start":930159,"end":930359,"confidence":1,"speaker":"A"},{"text":"a","start":930359,"end":930480,"confidence":0.9794922,"speaker":"A"},{"text":"regular","start":930480,"end":930760,"confidence":1,"speaker":"A"},{"text":"like","start":930760,"end":930960,"confidence":0.9975586,"speaker":"A"},{"text":"email","start":930960,"end":931240,"confidence":1,"speaker":"A"},{"text":"address","start":931240,"end":931520,"confidence":1,"speaker":"A"},{"text":"and","start":931520,"end":931760,"confidence":0.6791992,"speaker":"A"},{"text":"password","start":931760,"end":932320,"confidence":0.88378906,"speaker":"A"},{"text":"in","start":933040,"end":933440,"confidence":0.7763672,"speaker":"A"},{"text":"Heart","start":933680,"end":934000,"confidence":0.66796875,"speaker":"A"},{"text":"Twitch","start":934000,"end":934400,"confidence":0.9975586,"speaker":"A"},{"text":"on","start":934400,"end":934560,"confidence":1,"speaker":"A"},{"text":"the","start":934560,"end":934680,"confidence":1,"speaker":"A"},{"text":"website.","start":934680,"end":934960,"confidence":0.9995117,"speaker":"A"},{"text":"And","start":935840,"end":936120,"confidence":0.99902344,"speaker":"A"},{"text":"then","start":936120,"end":936280,"confidence":0.9995117,"speaker":"A"},{"text":"there's","start":936280,"end":936520,"confidence":0.8927409,"speaker":"A"},{"text":"a","start":936520,"end":936640,"confidence":0.9995117,"speaker":"A"},{"text":"little,","start":936640,"end":936840,"confidence":1,"speaker":"A"},{"text":"there's","start":936840,"end":937200,"confidence":0.9996745,"speaker":"A"},{"text":"a","start":937200,"end":937360,"confidence":0.9995117,"speaker":"A"},{"text":"site,","start":937360,"end":937640,"confidence":0.9995117,"speaker":"A"},{"text":"there's","start":937640,"end":937960,"confidence":0.99886066,"speaker":"A"},{"text":"a","start":937960,"end":938160,"confidence":0.9995117,"speaker":"A"},{"text":"part","start":938160,"end":938360,"confidence":1,"speaker":"A"},{"text":"of","start":938360,"end":938480,"confidence":1,"speaker":"A"},{"text":"the","start":938480,"end":938560,"confidence":1,"speaker":"A"},{"text":"site","start":938560,"end":938720,"confidence":1,"speaker":"A"},{"text":"where","start":938720,"end":938920,"confidence":1,"speaker":"A"},{"text":"you","start":938920,"end":939040,"confidence":1,"speaker":"A"},{"text":"can","start":939040,"end":939280,"confidence":1,"speaker":"A"},{"text":"sign","start":939840,"end":940120,"confidence":1,"speaker":"A"},{"text":"into","start":940120,"end":940360,"confidence":0.8144531,"speaker":"A"},{"text":"CloudKit","start":940360,"end":941120,"confidence":0.9995117,"speaker":"A"},{"text":"and","start":942180,"end":942300,"confidence":0.94628906,"speaker":"A"},{"text":"then","start":942300,"end":942500,"confidence":0.99902344,"speaker":"A"},{"text":"from","start":942500,"end":942740,"confidence":1,"speaker":"A"},{"text":"there","start":942740,"end":943060,"confidence":0.9995117,"speaker":"A"},{"text":"you","start":944180,"end":944540,"confidence":0.9526367,"speaker":"A"},{"text":"can,","start":944540,"end":944900,"confidence":1,"speaker":"A"},{"text":"because,","start":945860,"end":946260,"confidence":0.8623047,"speaker":"A"},{"text":"because","start":946260,"end":946540,"confidence":0.99853516,"speaker":"A"},{"text":"of","start":946540,"end":946700,"confidence":0.9897461,"speaker":"A"},{"text":"the","start":946700,"end":946820,"confidence":0.9980469,"speaker":"A"},{"text":"CloudKit","start":946820,"end":947340,"confidence":0.99438477,"speaker":"A"},{"text":"JavaScript","start":947340,"end":947980,"confidence":0.9984538,"speaker":"A"},{"text":"library,","start":947980,"end":948380,"confidence":0.9995117,"speaker":"A"},{"text":"you","start":948380,"end":948540,"confidence":0.95751953,"speaker":"A"},{"text":"can","start":948540,"end":948660,"confidence":0.99902344,"speaker":"A"},{"text":"then","start":948660,"end":948820,"confidence":0.99853516,"speaker":"A"},{"text":"I","start":948820,"end":948980,"confidence":0.9951172,"speaker":"A"},{"text":"can","start":948980,"end":949100,"confidence":0.9975586,"speaker":"A"},{"text":"then","start":949100,"end":949300,"confidence":0.9951172,"speaker":"A"},{"text":"pull","start":949300,"end":949620,"confidence":0.99975586,"speaker":"A"},{"text":"the","start":949620,"end":949940,"confidence":0.9140625,"speaker":"A"},{"text":"all","start":952260,"end":952580,"confidence":0.99609375,"speaker":"A"},{"text":"the","start":952580,"end":952780,"confidence":0.99902344,"speaker":"A"},{"text":"devices","start":952780,"end":953220,"confidence":0.9992676,"speaker":"A"},{"text":"because","start":953220,"end":953540,"confidence":0.99902344,"speaker":"A"},{"text":"when","start":953540,"end":953740,"confidence":1,"speaker":"A"},{"text":"you","start":953740,"end":953900,"confidence":0.9995117,"speaker":"A"},{"text":"first","start":953900,"end":954100,"confidence":1,"speaker":"A"},{"text":"launch","start":954100,"end":954340,"confidence":1,"speaker":"A"},{"text":"the","start":954340,"end":954540,"confidence":0.9746094,"speaker":"A"},{"text":"app","start":954540,"end":954700,"confidence":0.9995117,"speaker":"A"},{"text":"on","start":954700,"end":954820,"confidence":0.9995117,"speaker":"A"},{"text":"the","start":954820,"end":954900,"confidence":0.9995117,"speaker":"A"},{"text":"Watch,","start":954900,"end":955100,"confidence":0.9897461,"speaker":"A"},{"text":"it","start":955100,"end":955340,"confidence":0.93408203,"speaker":"A"},{"text":"adds","start":955340,"end":955580,"confidence":0.9987793,"speaker":"A"},{"text":"your","start":955580,"end":955740,"confidence":0.9980469,"speaker":"A"},{"text":"watch","start":955740,"end":956020,"confidence":0.9995117,"speaker":"A"},{"text":"to","start":956340,"end":956620,"confidence":0.9995117,"speaker":"A"},{"text":"the","start":956620,"end":956740,"confidence":0.9995117,"speaker":"A"},{"text":"CloudKit","start":956740,"end":957300,"confidence":0.99609375,"speaker":"A"},{"text":"database.","start":957300,"end":957940,"confidence":0.9998372,"speaker":"A"},{"text":"And","start":958260,"end":958540,"confidence":0.9921875,"speaker":"A"},{"text":"then","start":958540,"end":958660,"confidence":0.9995117,"speaker":"A"},{"text":"I","start":958660,"end":958780,"confidence":0.99902344,"speaker":"A"},{"text":"could","start":958780,"end":958940,"confidence":0.66503906,"speaker":"A"},{"text":"pull","start":958940,"end":959140,"confidence":1,"speaker":"A"},{"text":"that","start":959140,"end":959300,"confidence":0.9975586,"speaker":"A"},{"text":"in","start":959300,"end":959540,"confidence":0.9980469,"speaker":"A"},{"text":"and","start":959540,"end":959740,"confidence":0.9995117,"speaker":"A"},{"text":"then","start":959740,"end":959900,"confidence":0.9970703,"speaker":"A"},{"text":"add","start":959900,"end":960060,"confidence":0.9995117,"speaker":"A"},{"text":"that","start":960060,"end":960220,"confidence":0.9995117,"speaker":"A"},{"text":"to","start":960220,"end":960380,"confidence":0.9995117,"speaker":"A"},{"text":"my","start":960380,"end":960540,"confidence":0.9995117,"speaker":"A"},{"text":"postgres","start":960540,"end":961140,"confidence":0.98583984,"speaker":"A"},{"text":"database.","start":961140,"end":961700,"confidence":1,"speaker":"A"},{"text":"So","start":961700,"end":961980,"confidence":0.99658203,"speaker":"A"},{"text":"then","start":961980,"end":962260,"confidence":0.9970703,"speaker":"A"},{"text":"there","start":962260,"end":962540,"confidence":1,"speaker":"A"},{"text":"is","start":962540,"end":962740,"confidence":0.9995117,"speaker":"A"},{"text":"no","start":962740,"end":962940,"confidence":0.9995117,"speaker":"A"},{"text":"need","start":962940,"end":963140,"confidence":1,"speaker":"A"},{"text":"for","start":963140,"end":963380,"confidence":0.9995117,"speaker":"A"},{"text":"authentication","start":963380,"end":964180,"confidence":0.9998779,"speaker":"A"},{"text":"because","start":964740,"end":965140,"confidence":0.9995117,"speaker":"A"},{"text":"I","start":965220,"end":965500,"confidence":0.9980469,"speaker":"A"},{"text":"already","start":965500,"end":965700,"confidence":1,"speaker":"A"},{"text":"have","start":965700,"end":965900,"confidence":0.99853516,"speaker":"A"},{"text":"the","start":965900,"end":966060,"confidence":0.9995117,"speaker":"A"},{"text":"CloudKit,","start":966060,"end":966740,"confidence":0.99975586,"speaker":"A"},{"text":"the","start":967720,"end":967880,"confidence":0.9663086,"speaker":"A"},{"text":"device","start":967880,"end":968280,"confidence":0.9992676,"speaker":"A"},{"text":"added","start":968280,"end":968600,"confidence":0.9995117,"speaker":"A"},{"text":"in","start":969000,"end":969280,"confidence":0.9995117,"speaker":"A"},{"text":"my","start":969280,"end":969480,"confidence":0.9926758,"speaker":"A"},{"text":"postgres","start":969480,"end":970000,"confidence":0.89941406,"speaker":"A"},{"text":"database.","start":970000,"end":970400,"confidence":0.9998372,"speaker":"A"},{"text":"So","start":970400,"end":970520,"confidence":0.8930664,"speaker":"A"},{"text":"it's","start":970520,"end":970720,"confidence":0.87093097,"speaker":"A"},{"text":"kind","start":970720,"end":970840,"confidence":0.93603516,"speaker":"A"},{"text":"of","start":970840,"end":970960,"confidence":0.859375,"speaker":"A"},{"text":"like","start":970960,"end":971120,"confidence":0.9736328,"speaker":"A"},{"text":"knows,","start":971120,"end":971440,"confidence":0.94555664,"speaker":"A"},{"text":"oh","start":971440,"end":971680,"confidence":0.97143555,"speaker":"A"},{"text":"yeah,","start":971680,"end":972040,"confidence":0.9983724,"speaker":"A"},{"text":"this","start":972200,"end":972480,"confidence":0.9995117,"speaker":"A"},{"text":"is","start":972480,"end":972720,"confidence":0.99902344,"speaker":"A"},{"text":"Leo's","start":972720,"end":973280,"confidence":0.9902344,"speaker":"A"},{"text":"watch,","start":973280,"end":973560,"confidence":0.99853516,"speaker":"A"},{"text":"he","start":974040,"end":974320,"confidence":0.99902344,"speaker":"A"},{"text":"doesn't","start":974320,"end":974520,"confidence":0.9996745,"speaker":"A"},{"text":"need","start":974520,"end":974640,"confidence":0.99902344,"speaker":"A"},{"text":"to","start":974640,"end":974840,"confidence":0.9863281,"speaker":"A"},{"text":"authenticate.","start":974840,"end":975520,"confidence":0.9996338,"speaker":"A"}]},{"text":"And that way we can link devices to accounts without having to do any sort of login process. And so this was my use case for doing server side. Essentially CloudKit was I could call the CloudKit web server based on that person's web authentication token, which we'll get all into later. I then pull that information in. So.","start":975520,"end":1002450,"confidence":0.9116211,"words":[{"text":"And","start":975520,"end":975760,"confidence":0.9116211,"speaker":"A"},{"text":"that","start":975760,"end":975920,"confidence":0.99365234,"speaker":"A"},{"text":"way","start":975920,"end":976120,"confidence":0.99853516,"speaker":"A"},{"text":"we","start":976120,"end":976320,"confidence":0.9995117,"speaker":"A"},{"text":"can","start":976320,"end":976520,"confidence":0.9995117,"speaker":"A"},{"text":"link","start":976520,"end":976800,"confidence":0.99975586,"speaker":"A"},{"text":"devices","start":976800,"end":977240,"confidence":0.9995117,"speaker":"A"},{"text":"to","start":977240,"end":977520,"confidence":0.9614258,"speaker":"A"},{"text":"accounts","start":977520,"end":978200,"confidence":0.9980469,"speaker":"A"},{"text":"without","start":978280,"end":978680,"confidence":0.9995117,"speaker":"A"},{"text":"having","start":978680,"end":978960,"confidence":0.9995117,"speaker":"A"},{"text":"to","start":978960,"end":979120,"confidence":0.9995117,"speaker":"A"},{"text":"do","start":979120,"end":979280,"confidence":0.9995117,"speaker":"A"},{"text":"any","start":979280,"end":979440,"confidence":0.9995117,"speaker":"A"},{"text":"sort","start":979440,"end":979640,"confidence":0.99625653,"speaker":"A"},{"text":"of","start":979640,"end":979760,"confidence":0.9951172,"speaker":"A"},{"text":"login","start":979760,"end":980200,"confidence":0.984375,"speaker":"A"},{"text":"process.","start":980200,"end":980520,"confidence":0.9995117,"speaker":"A"},{"text":"And","start":981080,"end":981360,"confidence":0.9008789,"speaker":"A"},{"text":"so","start":981360,"end":981600,"confidence":0.59228516,"speaker":"A"},{"text":"this","start":981600,"end":981840,"confidence":0.9995117,"speaker":"A"},{"text":"was","start":981840,"end":982000,"confidence":0.9951172,"speaker":"A"},{"text":"my","start":982000,"end":982200,"confidence":0.99902344,"speaker":"A"},{"text":"use","start":982200,"end":982440,"confidence":0.9916992,"speaker":"A"},{"text":"case","start":982440,"end":982760,"confidence":0.9995117,"speaker":"A"},{"text":"for","start":982919,"end":983320,"confidence":0.9995117,"speaker":"A"},{"text":"doing","start":983800,"end":984200,"confidence":0.99902344,"speaker":"A"},{"text":"server","start":985160,"end":985680,"confidence":0.71899414,"speaker":"A"},{"text":"side.","start":985680,"end":985960,"confidence":0.9086914,"speaker":"A"},{"text":"Essentially","start":986040,"end":986680,"confidence":0.9888916,"speaker":"A"},{"text":"CloudKit","start":987000,"end":987720,"confidence":0.87207,"speaker":"A"},{"text":"was","start":987720,"end":988000,"confidence":0.98583984,"speaker":"A"},{"text":"I","start":988000,"end":988240,"confidence":0.99902344,"speaker":"A"},{"text":"could","start":988240,"end":988400,"confidence":0.99365234,"speaker":"A"},{"text":"call","start":988400,"end":988600,"confidence":0.9995117,"speaker":"A"},{"text":"the","start":988600,"end":988800,"confidence":0.99853516,"speaker":"A"},{"text":"CloudKit","start":988800,"end":989360,"confidence":0.9609375,"speaker":"A"},{"text":"web","start":989360,"end":989560,"confidence":0.9902344,"speaker":"A"},{"text":"server","start":989560,"end":990040,"confidence":0.99902344,"speaker":"A"},{"text":"based","start":993410,"end":993610,"confidence":0.98876953,"speaker":"A"},{"text":"on","start":993610,"end":993850,"confidence":1,"speaker":"A"},{"text":"that","start":993850,"end":994050,"confidence":0.9995117,"speaker":"A"},{"text":"person's","start":994050,"end":994690,"confidence":0.99690753,"speaker":"A"},{"text":"web","start":995570,"end":995970,"confidence":0.9992676,"speaker":"A"},{"text":"authentication","start":995970,"end":996610,"confidence":0.9998779,"speaker":"A"},{"text":"token,","start":996610,"end":996970,"confidence":0.9998372,"speaker":"A"},{"text":"which","start":996970,"end":997130,"confidence":0.9995117,"speaker":"A"},{"text":"we'll","start":997130,"end":997330,"confidence":0.9316406,"speaker":"A"},{"text":"get","start":997330,"end":997490,"confidence":0.99902344,"speaker":"A"},{"text":"all","start":997490,"end":997730,"confidence":0.74365234,"speaker":"A"},{"text":"into","start":997730,"end":998010,"confidence":0.99072266,"speaker":"A"},{"text":"later.","start":998010,"end":998370,"confidence":0.99902344,"speaker":"A"},{"text":"I","start":998530,"end":998850,"confidence":0.5698242,"speaker":"A"},{"text":"then","start":998850,"end":999050,"confidence":0.91748047,"speaker":"A"},{"text":"pull","start":999050,"end":999250,"confidence":0.99975586,"speaker":"A"},{"text":"that","start":999250,"end":999410,"confidence":0.9980469,"speaker":"A"},{"text":"information","start":999410,"end":999730,"confidence":0.9995117,"speaker":"A"},{"text":"in.","start":999970,"end":1000370,"confidence":0.9824219,"speaker":"A"},{"text":"So.","start":1002050,"end":1002450,"confidence":0.8515625,"speaker":"A"}]},{"text":"Cool.","start":1007250,"end":1007730,"confidence":0.9333496,"words":[{"text":"Cool.","start":1007250,"end":1007730,"confidence":0.9333496,"speaker":"A"}]},{"text":"Just checking if anybody's having issues. It doesn't look like it. So that's good to know. So that was the private database piece, but I actually think a much more useful case would be the public database because the idea would be is that you'd have some sort of app that would use central repository of data that it can pull information from. And I'm looking at both of these with Bushel and then an RSS reader I'm building called Celestra with Bushel.","start":1010770,"end":1045150,"confidence":0.99121094,"words":[{"text":"Just","start":1010770,"end":1011050,"confidence":0.99121094,"speaker":"A"},{"text":"checking","start":1011050,"end":1011370,"confidence":0.9980469,"speaker":"A"},{"text":"if","start":1011370,"end":1011530,"confidence":0.99853516,"speaker":"A"},{"text":"anybody's","start":1011530,"end":1012050,"confidence":0.94539386,"speaker":"A"},{"text":"having","start":1012050,"end":1012210,"confidence":0.9995117,"speaker":"A"},{"text":"issues.","start":1012210,"end":1012530,"confidence":0.99853516,"speaker":"A"},{"text":"It","start":1012530,"end":1012770,"confidence":0.5439453,"speaker":"A"},{"text":"doesn't","start":1012770,"end":1013050,"confidence":0.9983724,"speaker":"A"},{"text":"look","start":1013050,"end":1013210,"confidence":0.99902344,"speaker":"A"},{"text":"like","start":1013210,"end":1013370,"confidence":0.99853516,"speaker":"A"},{"text":"it.","start":1013370,"end":1013650,"confidence":0.9975586,"speaker":"A"},{"text":"So","start":1013650,"end":1014050,"confidence":0.8925781,"speaker":"A"},{"text":"that's","start":1014690,"end":1015050,"confidence":0.98014325,"speaker":"A"},{"text":"good","start":1015050,"end":1015210,"confidence":0.9995117,"speaker":"A"},{"text":"to","start":1015210,"end":1015370,"confidence":0.9980469,"speaker":"A"},{"text":"know.","start":1015370,"end":1015650,"confidence":0.9975586,"speaker":"A"},{"text":"So","start":1017170,"end":1017410,"confidence":0.9707031,"speaker":"A"},{"text":"that","start":1017410,"end":1017530,"confidence":0.98779297,"speaker":"A"},{"text":"was","start":1017530,"end":1017690,"confidence":0.9995117,"speaker":"A"},{"text":"the","start":1017690,"end":1017850,"confidence":0.9995117,"speaker":"A"},{"text":"private","start":1017850,"end":1018090,"confidence":0.9995117,"speaker":"A"},{"text":"database","start":1018090,"end":1018690,"confidence":0.9998372,"speaker":"A"},{"text":"piece,","start":1018690,"end":1019090,"confidence":0.99576825,"speaker":"A"},{"text":"but","start":1019950,"end":1020070,"confidence":0.97558594,"speaker":"A"},{"text":"I","start":1020070,"end":1020230,"confidence":0.99853516,"speaker":"A"},{"text":"actually","start":1020230,"end":1020470,"confidence":0.9970703,"speaker":"A"},{"text":"think","start":1020470,"end":1020790,"confidence":0.9995117,"speaker":"A"},{"text":"a","start":1020790,"end":1021030,"confidence":0.9921875,"speaker":"A"},{"text":"much","start":1021030,"end":1021230,"confidence":0.9946289,"speaker":"A"},{"text":"more","start":1021230,"end":1021470,"confidence":1,"speaker":"A"},{"text":"useful","start":1021470,"end":1021910,"confidence":0.99975586,"speaker":"A"},{"text":"case","start":1021910,"end":1022270,"confidence":0.9995117,"speaker":"A"},{"text":"would","start":1022670,"end":1022990,"confidence":1,"speaker":"A"},{"text":"be","start":1022990,"end":1023270,"confidence":1,"speaker":"A"},{"text":"the","start":1023270,"end":1023510,"confidence":0.9995117,"speaker":"A"},{"text":"public","start":1023510,"end":1023750,"confidence":0.9995117,"speaker":"A"},{"text":"database","start":1023750,"end":1024430,"confidence":0.99934894,"speaker":"A"},{"text":"because","start":1024990,"end":1025390,"confidence":0.9946289,"speaker":"A"},{"text":"the","start":1026830,"end":1027150,"confidence":0.99853516,"speaker":"A"},{"text":"idea","start":1027150,"end":1027550,"confidence":0.9758301,"speaker":"A"},{"text":"would","start":1027550,"end":1027750,"confidence":0.99658203,"speaker":"A"},{"text":"be","start":1027750,"end":1027950,"confidence":0.99902344,"speaker":"A"},{"text":"is","start":1027950,"end":1028150,"confidence":0.93359375,"speaker":"A"},{"text":"that","start":1028150,"end":1028310,"confidence":0.99853516,"speaker":"A"},{"text":"you'd","start":1028310,"end":1028630,"confidence":0.96516925,"speaker":"A"},{"text":"have","start":1028630,"end":1028910,"confidence":1,"speaker":"A"},{"text":"some","start":1029710,"end":1029990,"confidence":0.9995117,"speaker":"A"},{"text":"sort","start":1029990,"end":1030230,"confidence":0.99609375,"speaker":"A"},{"text":"of","start":1030230,"end":1030390,"confidence":0.9975586,"speaker":"A"},{"text":"app","start":1030390,"end":1030670,"confidence":0.99902344,"speaker":"A"},{"text":"that","start":1030670,"end":1030950,"confidence":0.9995117,"speaker":"A"},{"text":"would","start":1030950,"end":1031150,"confidence":0.9970703,"speaker":"A"},{"text":"use","start":1031150,"end":1031470,"confidence":0.99902344,"speaker":"A"},{"text":"central","start":1031550,"end":1031950,"confidence":0.9995117,"speaker":"A"},{"text":"repository","start":1031950,"end":1032790,"confidence":0.99694824,"speaker":"A"},{"text":"of","start":1032790,"end":1032990,"confidence":0.99853516,"speaker":"A"},{"text":"data","start":1032990,"end":1033310,"confidence":0.99902344,"speaker":"A"},{"text":"that","start":1035470,"end":1035790,"confidence":0.99902344,"speaker":"A"},{"text":"it","start":1035790,"end":1035950,"confidence":0.63134766,"speaker":"A"},{"text":"can","start":1035950,"end":1036070,"confidence":0.9980469,"speaker":"A"},{"text":"pull","start":1036070,"end":1036390,"confidence":0.99975586,"speaker":"A"},{"text":"information","start":1036390,"end":1036750,"confidence":1,"speaker":"A"},{"text":"from.","start":1036990,"end":1037390,"confidence":0.9995117,"speaker":"A"},{"text":"And","start":1037790,"end":1038110,"confidence":0.91259766,"speaker":"A"},{"text":"I'm","start":1038110,"end":1038390,"confidence":0.99104816,"speaker":"A"},{"text":"looking","start":1038390,"end":1038550,"confidence":0.9902344,"speaker":"A"},{"text":"at","start":1038550,"end":1038710,"confidence":0.99902344,"speaker":"A"},{"text":"both","start":1038710,"end":1038870,"confidence":1,"speaker":"A"},{"text":"of","start":1038870,"end":1039030,"confidence":0.9995117,"speaker":"A"},{"text":"these","start":1039030,"end":1039310,"confidence":0.9995117,"speaker":"A"},{"text":"with","start":1039310,"end":1039710,"confidence":0.99902344,"speaker":"A"},{"text":"Bushel","start":1039950,"end":1040590,"confidence":0.90722656,"speaker":"A"},{"text":"and","start":1040590,"end":1040790,"confidence":0.9975586,"speaker":"A"},{"text":"then","start":1040790,"end":1040950,"confidence":0.9584961,"speaker":"A"},{"text":"an","start":1040950,"end":1041190,"confidence":0.98291016,"speaker":"A"},{"text":"RSS","start":1041190,"end":1041670,"confidence":0.9987793,"speaker":"A"},{"text":"reader","start":1041670,"end":1042070,"confidence":0.9975586,"speaker":"A"},{"text":"I'm","start":1042070,"end":1042270,"confidence":0.93929034,"speaker":"A"},{"text":"building","start":1042270,"end":1042430,"confidence":0.9995117,"speaker":"A"},{"text":"called","start":1042430,"end":1042630,"confidence":0.9584961,"speaker":"A"},{"text":"Celestra","start":1042630,"end":1043310,"confidence":0.9358724,"speaker":"A"},{"text":"with","start":1044190,"end":1044510,"confidence":0.98535156,"speaker":"A"},{"text":"Bushel.","start":1044510,"end":1045150,"confidence":0.9350586,"speaker":"A"}]},{"text":"The. The way it's built right now is I have this concept of hubs and you can plug in a URL and that URL would provide or some sort of service. That service would then provide the Entire List of macOS restore images that are available.","start":1046199,"end":1061959,"confidence":0.84375,"words":[{"text":"The.","start":1046199,"end":1046439,"confidence":0.84375,"speaker":"A"},{"text":"The","start":1046679,"end":1046959,"confidence":0.9980469,"speaker":"A"},{"text":"way","start":1046959,"end":1047119,"confidence":1,"speaker":"A"},{"text":"it's","start":1047119,"end":1047319,"confidence":0.9996745,"speaker":"A"},{"text":"built","start":1047319,"end":1047559,"confidence":0.8929036,"speaker":"A"},{"text":"right","start":1047559,"end":1047759,"confidence":0.9995117,"speaker":"A"},{"text":"now","start":1047759,"end":1047959,"confidence":0.9995117,"speaker":"A"},{"text":"is","start":1047959,"end":1048199,"confidence":0.9667969,"speaker":"A"},{"text":"I","start":1048199,"end":1048359,"confidence":0.99902344,"speaker":"A"},{"text":"have","start":1048359,"end":1048479,"confidence":0.9975586,"speaker":"A"},{"text":"this","start":1048479,"end":1048679,"confidence":0.9995117,"speaker":"A"},{"text":"concept","start":1048679,"end":1049079,"confidence":0.9786784,"speaker":"A"},{"text":"of","start":1049079,"end":1049319,"confidence":0.9995117,"speaker":"A"},{"text":"hubs","start":1049319,"end":1049719,"confidence":0.9838867,"speaker":"A"},{"text":"and","start":1050679,"end":1051079,"confidence":0.96240234,"speaker":"A"},{"text":"you","start":1051159,"end":1051439,"confidence":1,"speaker":"A"},{"text":"can","start":1051439,"end":1051599,"confidence":0.99902344,"speaker":"A"},{"text":"plug","start":1051599,"end":1051799,"confidence":1,"speaker":"A"},{"text":"in","start":1051799,"end":1051919,"confidence":0.9951172,"speaker":"A"},{"text":"a","start":1051919,"end":1052079,"confidence":0.99072266,"speaker":"A"},{"text":"URL","start":1052079,"end":1052639,"confidence":0.9992676,"speaker":"A"},{"text":"and","start":1052639,"end":1052839,"confidence":0.9628906,"speaker":"A"},{"text":"that","start":1052839,"end":1052959,"confidence":0.99902344,"speaker":"A"},{"text":"URL","start":1052959,"end":1053439,"confidence":0.9367676,"speaker":"A"},{"text":"would","start":1053439,"end":1053719,"confidence":0.99658203,"speaker":"A"},{"text":"provide","start":1053719,"end":1054039,"confidence":1,"speaker":"A"},{"text":"or","start":1054039,"end":1054399,"confidence":0.99902344,"speaker":"A"},{"text":"some","start":1054399,"end":1054679,"confidence":0.97216797,"speaker":"A"},{"text":"sort","start":1054679,"end":1054919,"confidence":0.9941406,"speaker":"A"},{"text":"of","start":1054919,"end":1055079,"confidence":0.99902344,"speaker":"A"},{"text":"service.","start":1055079,"end":1055399,"confidence":0.99902344,"speaker":"A"},{"text":"That","start":1055959,"end":1056359,"confidence":0.9980469,"speaker":"A"},{"text":"service","start":1056599,"end":1056999,"confidence":0.9980469,"speaker":"A"},{"text":"would","start":1056999,"end":1057279,"confidence":0.9941406,"speaker":"A"},{"text":"then","start":1057279,"end":1057479,"confidence":0.9916992,"speaker":"A"},{"text":"provide","start":1057479,"end":1057799,"confidence":1,"speaker":"A"},{"text":"the","start":1058359,"end":1058639,"confidence":0.9995117,"speaker":"A"},{"text":"Entire","start":1058639,"end":1058999,"confidence":0.99975586,"speaker":"A"},{"text":"List","start":1058999,"end":1059279,"confidence":0.9995117,"speaker":"A"},{"text":"of","start":1059279,"end":1059639,"confidence":0.99853516,"speaker":"A"},{"text":"macOS","start":1059719,"end":1060439,"confidence":0.76636,"speaker":"A"},{"text":"restore","start":1060439,"end":1060839,"confidence":0.98168945,"speaker":"A"},{"text":"images","start":1060839,"end":1061278,"confidence":0.9987793,"speaker":"A"},{"text":"that","start":1061278,"end":1061479,"confidence":0.9995117,"speaker":"A"},{"text":"are","start":1061479,"end":1061638,"confidence":0.9995117,"speaker":"A"},{"text":"available.","start":1061638,"end":1061959,"confidence":0.9995117,"speaker":"A"}]},{"text":"But then I realized like really there's only one location for those and each service is just going to be using the same URLs anyway. So if I had one central repository or one central database because they all pull from Apple, I can then parse the web for those restore images and then store them in CloudKit and then that way Bushel can then pull those from one single repository. And all I would have to do, and what I'm doing now is running basically a GitHub action or you could do like a Cron job where it would run on Ubuntu, wouldn't even need a Mac and it would download and scrape the web for restore images and storm in the public database. It's the same idea with Celestra. It's an RSS reader.","start":1064119,"end":1109110,"confidence":0.9941406,"words":[{"text":"But","start":1064119,"end":1064399,"confidence":0.9941406,"speaker":"A"},{"text":"then","start":1064399,"end":1064559,"confidence":0.9995117,"speaker":"A"},{"text":"I","start":1064559,"end":1064719,"confidence":0.9995117,"speaker":"A"},{"text":"realized","start":1064719,"end":1065079,"confidence":0.9863281,"speaker":"A"},{"text":"like","start":1065079,"end":1065319,"confidence":0.90283203,"speaker":"A"},{"text":"really","start":1065319,"end":1065559,"confidence":0.9970703,"speaker":"A"},{"text":"there's","start":1065559,"end":1065839,"confidence":0.9889323,"speaker":"A"},{"text":"only","start":1065839,"end":1065999,"confidence":0.9995117,"speaker":"A"},{"text":"one","start":1065999,"end":1066199,"confidence":0.9995117,"speaker":"A"},{"text":"location","start":1066199,"end":1066679,"confidence":1,"speaker":"A"},{"text":"for","start":1066679,"end":1066919,"confidence":0.9995117,"speaker":"A"},{"text":"those","start":1066919,"end":1067239,"confidence":0.9995117,"speaker":"A"},{"text":"and","start":1067319,"end":1067719,"confidence":0.98876953,"speaker":"A"},{"text":"each","start":1067719,"end":1068079,"confidence":0.9824219,"speaker":"A"},{"text":"service","start":1068079,"end":1068399,"confidence":0.9951172,"speaker":"A"},{"text":"is","start":1068399,"end":1068639,"confidence":0.99853516,"speaker":"A"},{"text":"just","start":1068639,"end":1068799,"confidence":0.99609375,"speaker":"A"},{"text":"going","start":1068799,"end":1068919,"confidence":0.8798828,"speaker":"A"},{"text":"to","start":1068919,"end":1068999,"confidence":0.99902344,"speaker":"A"},{"text":"be","start":1068999,"end":1069079,"confidence":0.99853516,"speaker":"A"},{"text":"using","start":1069079,"end":1069319,"confidence":0.9995117,"speaker":"A"},{"text":"the","start":1069319,"end":1069559,"confidence":0.9995117,"speaker":"A"},{"text":"same","start":1069559,"end":1069719,"confidence":0.9995117,"speaker":"A"},{"text":"URLs","start":1069719,"end":1070359,"confidence":0.92261,"speaker":"A"},{"text":"anyway.","start":1070359,"end":1070839,"confidence":0.99731445,"speaker":"A"},{"text":"So","start":1071970,"end":1072050,"confidence":0.92822266,"speaker":"A"},{"text":"if","start":1072050,"end":1072170,"confidence":0.9995117,"speaker":"A"},{"text":"I","start":1072170,"end":1072330,"confidence":0.9995117,"speaker":"A"},{"text":"had","start":1072330,"end":1072570,"confidence":0.9975586,"speaker":"A"},{"text":"one","start":1072570,"end":1072850,"confidence":0.9995117,"speaker":"A"},{"text":"central","start":1072850,"end":1073170,"confidence":1,"speaker":"A"},{"text":"repository","start":1073250,"end":1074050,"confidence":0.9127197,"speaker":"A"},{"text":"or","start":1074050,"end":1074250,"confidence":0.99853516,"speaker":"A"},{"text":"one","start":1074250,"end":1074450,"confidence":0.9970703,"speaker":"A"},{"text":"central","start":1074450,"end":1074770,"confidence":1,"speaker":"A"},{"text":"database","start":1074770,"end":1075490,"confidence":1,"speaker":"A"},{"text":"because","start":1076850,"end":1077170,"confidence":0.99365234,"speaker":"A"},{"text":"they","start":1077170,"end":1077370,"confidence":0.9975586,"speaker":"A"},{"text":"all","start":1077370,"end":1077530,"confidence":0.99902344,"speaker":"A"},{"text":"pull","start":1077530,"end":1077770,"confidence":0.99975586,"speaker":"A"},{"text":"from","start":1077770,"end":1077970,"confidence":0.9995117,"speaker":"A"},{"text":"Apple,","start":1077970,"end":1078450,"confidence":0.9995117,"speaker":"A"},{"text":"I","start":1078690,"end":1079010,"confidence":0.99853516,"speaker":"A"},{"text":"can","start":1079010,"end":1079210,"confidence":0.99365234,"speaker":"A"},{"text":"then","start":1079210,"end":1079490,"confidence":0.98828125,"speaker":"A"},{"text":"parse","start":1079650,"end":1080250,"confidence":0.8129883,"speaker":"A"},{"text":"the","start":1080250,"end":1080490,"confidence":0.9995117,"speaker":"A"},{"text":"web","start":1080490,"end":1080850,"confidence":0.99975586,"speaker":"A"},{"text":"for","start":1081090,"end":1081410,"confidence":0.59033203,"speaker":"A"},{"text":"those","start":1081410,"end":1081690,"confidence":0.99902344,"speaker":"A"},{"text":"restore","start":1081690,"end":1082210,"confidence":0.98779297,"speaker":"A"},{"text":"images","start":1082210,"end":1082690,"confidence":0.99780273,"speaker":"A"},{"text":"and","start":1082690,"end":1082930,"confidence":0.99072266,"speaker":"A"},{"text":"then","start":1082930,"end":1083090,"confidence":0.99658203,"speaker":"A"},{"text":"store","start":1083090,"end":1083370,"confidence":0.9736328,"speaker":"A"},{"text":"them","start":1083370,"end":1083530,"confidence":0.9238281,"speaker":"A"},{"text":"in","start":1083530,"end":1083650,"confidence":0.98779297,"speaker":"A"},{"text":"CloudKit","start":1083650,"end":1084210,"confidence":0.94812,"speaker":"A"},{"text":"and","start":1084210,"end":1084370,"confidence":0.8354492,"speaker":"A"},{"text":"then","start":1084370,"end":1084530,"confidence":0.9873047,"speaker":"A"},{"text":"that","start":1084530,"end":1084770,"confidence":0.9980469,"speaker":"A"},{"text":"way","start":1084770,"end":1085090,"confidence":0.99853516,"speaker":"A"},{"text":"Bushel","start":1085410,"end":1086010,"confidence":0.8808594,"speaker":"A"},{"text":"can","start":1086010,"end":1086170,"confidence":0.9501953,"speaker":"A"},{"text":"then","start":1086170,"end":1086450,"confidence":0.95751953,"speaker":"A"},{"text":"pull","start":1087570,"end":1087930,"confidence":0.9995117,"speaker":"A"},{"text":"those","start":1087930,"end":1088210,"confidence":0.9975586,"speaker":"A"},{"text":"from","start":1088210,"end":1088530,"confidence":1,"speaker":"A"},{"text":"one","start":1088530,"end":1088770,"confidence":0.9995117,"speaker":"A"},{"text":"single","start":1088770,"end":1089090,"confidence":1,"speaker":"A"},{"text":"repository.","start":1089090,"end":1089970,"confidence":0.9998779,"speaker":"A"},{"text":"And","start":1090210,"end":1090490,"confidence":0.86572266,"speaker":"A"},{"text":"all","start":1090490,"end":1090650,"confidence":0.99853516,"speaker":"A"},{"text":"I","start":1090650,"end":1090770,"confidence":0.98291016,"speaker":"A"},{"text":"would","start":1090770,"end":1090930,"confidence":0.98583984,"speaker":"A"},{"text":"have","start":1090930,"end":1091090,"confidence":1,"speaker":"A"},{"text":"to","start":1091090,"end":1091210,"confidence":0.99902344,"speaker":"A"},{"text":"do,","start":1091210,"end":1091450,"confidence":0.9995117,"speaker":"A"},{"text":"and","start":1091450,"end":1091770,"confidence":0.64404297,"speaker":"A"},{"text":"what","start":1091770,"end":1092010,"confidence":0.9995117,"speaker":"A"},{"text":"I'm","start":1092010,"end":1092210,"confidence":0.99934894,"speaker":"A"},{"text":"doing","start":1092210,"end":1092410,"confidence":1,"speaker":"A"},{"text":"now","start":1092410,"end":1092690,"confidence":0.99853516,"speaker":"A"},{"text":"is","start":1092690,"end":1092930,"confidence":0.99902344,"speaker":"A"},{"text":"running","start":1092930,"end":1093370,"confidence":0.99121094,"speaker":"A"},{"text":"basically","start":1093370,"end":1093850,"confidence":0.998291,"speaker":"A"},{"text":"a","start":1093850,"end":1094090,"confidence":0.9951172,"speaker":"A"},{"text":"GitHub","start":1094090,"end":1094490,"confidence":0.9991862,"speaker":"A"},{"text":"action","start":1094490,"end":1094690,"confidence":1,"speaker":"A"},{"text":"or","start":1094690,"end":1094850,"confidence":0.98828125,"speaker":"A"},{"text":"you","start":1094850,"end":1094930,"confidence":0.91503906,"speaker":"A"},{"text":"could","start":1094930,"end":1095050,"confidence":0.8876953,"speaker":"A"},{"text":"do","start":1095050,"end":1095210,"confidence":0.99853516,"speaker":"A"},{"text":"like","start":1095210,"end":1095370,"confidence":0.8642578,"speaker":"A"},{"text":"a","start":1095370,"end":1095490,"confidence":0.9868164,"speaker":"A"},{"text":"Cron","start":1095490,"end":1095770,"confidence":0.97875977,"speaker":"A"},{"text":"job","start":1095770,"end":1096050,"confidence":1,"speaker":"A"},{"text":"where","start":1096450,"end":1096850,"confidence":0.9995117,"speaker":"A"},{"text":"it","start":1096850,"end":1097130,"confidence":0.99560547,"speaker":"A"},{"text":"would","start":1097130,"end":1097290,"confidence":1,"speaker":"A"},{"text":"run","start":1097290,"end":1097450,"confidence":0.9995117,"speaker":"A"},{"text":"on","start":1097450,"end":1097610,"confidence":0.9824219,"speaker":"A"},{"text":"Ubuntu,","start":1097610,"end":1098090,"confidence":0.8498047,"speaker":"A"},{"text":"wouldn't","start":1098090,"end":1098370,"confidence":0.9715576,"speaker":"A"},{"text":"even","start":1098370,"end":1098490,"confidence":0.99853516,"speaker":"A"},{"text":"need","start":1098490,"end":1098650,"confidence":0.99853516,"speaker":"A"},{"text":"a","start":1098650,"end":1098810,"confidence":0.99853516,"speaker":"A"},{"text":"Mac","start":1098810,"end":1099090,"confidence":0.9992676,"speaker":"A"},{"text":"and","start":1099090,"end":1099290,"confidence":0.96240234,"speaker":"A"},{"text":"it","start":1099290,"end":1099450,"confidence":0.99853516,"speaker":"A"},{"text":"would","start":1099450,"end":1099730,"confidence":0.9995117,"speaker":"A"},{"text":"download","start":1099890,"end":1100490,"confidence":1,"speaker":"A"},{"text":"and","start":1100490,"end":1100730,"confidence":0.59228516,"speaker":"A"},{"text":"scrape","start":1100730,"end":1101130,"confidence":0.8902588,"speaker":"A"},{"text":"the","start":1101130,"end":1101290,"confidence":0.9995117,"speaker":"A"},{"text":"web","start":1101290,"end":1101530,"confidence":0.9995117,"speaker":"A"},{"text":"for","start":1101530,"end":1101770,"confidence":0.9970703,"speaker":"A"},{"text":"restore","start":1101770,"end":1102250,"confidence":0.9777832,"speaker":"A"},{"text":"images","start":1102250,"end":1102650,"confidence":0.99731445,"speaker":"A"},{"text":"and","start":1102650,"end":1103000,"confidence":0.52197266,"speaker":"A"},{"text":"storm","start":1103070,"end":1103350,"confidence":0.92749023,"speaker":"A"},{"text":"in","start":1103350,"end":1103470,"confidence":0.9951172,"speaker":"A"},{"text":"the","start":1103470,"end":1103590,"confidence":0.99902344,"speaker":"A"},{"text":"public","start":1103590,"end":1103790,"confidence":1,"speaker":"A"},{"text":"database.","start":1103790,"end":1104430,"confidence":0.99820966,"speaker":"A"},{"text":"It's","start":1106350,"end":1106710,"confidence":0.9967448,"speaker":"A"},{"text":"the","start":1106710,"end":1106830,"confidence":0.9995117,"speaker":"A"},{"text":"same","start":1106830,"end":1106950,"confidence":1,"speaker":"A"},{"text":"idea","start":1106950,"end":1107230,"confidence":0.99902344,"speaker":"A"},{"text":"with","start":1107230,"end":1107350,"confidence":0.98779297,"speaker":"A"},{"text":"Celestra.","start":1107350,"end":1107910,"confidence":0.9313151,"speaker":"A"},{"text":"It's","start":1107910,"end":1108110,"confidence":0.99283856,"speaker":"A"},{"text":"an","start":1108110,"end":1108190,"confidence":0.73876953,"speaker":"A"},{"text":"RSS","start":1108190,"end":1108630,"confidence":0.9946289,"speaker":"A"},{"text":"reader.","start":1108630,"end":1109110,"confidence":0.99902344,"speaker":"A"}]},{"text":"What if I took those RSS RSS files in the web and just scrape them and then store them in a CloudKit database in a public database and then that way people can pull that up all through CloudKit.","start":1109110,"end":1122910,"confidence":0.9995117,"words":[{"text":"What","start":1109110,"end":1109270,"confidence":0.9995117,"speaker":"A"},{"text":"if","start":1109270,"end":1109430,"confidence":0.9995117,"speaker":"A"},{"text":"I","start":1109430,"end":1109630,"confidence":0.9995117,"speaker":"A"},{"text":"took","start":1109630,"end":1109870,"confidence":0.99902344,"speaker":"A"},{"text":"those","start":1109870,"end":1110070,"confidence":0.9946289,"speaker":"A"},{"text":"RSS","start":1110070,"end":1110590,"confidence":0.98535156,"speaker":"A"},{"text":"RSS","start":1112750,"end":1113310,"confidence":0.94921875,"speaker":"A"},{"text":"files","start":1113310,"end":1113670,"confidence":0.95703125,"speaker":"A"},{"text":"in","start":1113670,"end":1113830,"confidence":0.99365234,"speaker":"A"},{"text":"the","start":1113830,"end":1113950,"confidence":1,"speaker":"A"},{"text":"web","start":1113950,"end":1114150,"confidence":0.99975586,"speaker":"A"},{"text":"and","start":1114150,"end":1114350,"confidence":0.8354492,"speaker":"A"},{"text":"just","start":1114350,"end":1114630,"confidence":0.99853516,"speaker":"A"},{"text":"scrape","start":1114630,"end":1115110,"confidence":0.8651123,"speaker":"A"},{"text":"them","start":1115110,"end":1115270,"confidence":0.9995117,"speaker":"A"},{"text":"and","start":1115270,"end":1115430,"confidence":0.99902344,"speaker":"A"},{"text":"then","start":1115430,"end":1115630,"confidence":0.9970703,"speaker":"A"},{"text":"store","start":1115630,"end":1115950,"confidence":0.97753906,"speaker":"A"},{"text":"them","start":1115950,"end":1116070,"confidence":0.99902344,"speaker":"A"},{"text":"in","start":1116070,"end":1116190,"confidence":0.99902344,"speaker":"A"},{"text":"a","start":1116190,"end":1116270,"confidence":0.99902344,"speaker":"A"},{"text":"CloudKit","start":1116270,"end":1116830,"confidence":0.9890137,"speaker":"A"},{"text":"database","start":1116830,"end":1117470,"confidence":0.9996745,"speaker":"A"},{"text":"in","start":1118110,"end":1118430,"confidence":0.8745117,"speaker":"A"},{"text":"a","start":1118430,"end":1118590,"confidence":0.99902344,"speaker":"A"},{"text":"public","start":1118590,"end":1118750,"confidence":1,"speaker":"A"},{"text":"database","start":1118750,"end":1119390,"confidence":0.9998372,"speaker":"A"},{"text":"and","start":1119390,"end":1119550,"confidence":0.99316406,"speaker":"A"},{"text":"then","start":1119550,"end":1119710,"confidence":0.9741211,"speaker":"A"},{"text":"that","start":1119710,"end":1119910,"confidence":0.9995117,"speaker":"A"},{"text":"way","start":1119910,"end":1120110,"confidence":1,"speaker":"A"},{"text":"people","start":1120110,"end":1120390,"confidence":1,"speaker":"A"},{"text":"can","start":1120390,"end":1120750,"confidence":0.9995117,"speaker":"A"},{"text":"pull","start":1120750,"end":1121110,"confidence":1,"speaker":"A"},{"text":"that","start":1121110,"end":1121310,"confidence":0.99853516,"speaker":"A"},{"text":"up","start":1121310,"end":1121630,"confidence":0.99853516,"speaker":"A"},{"text":"all","start":1121630,"end":1121910,"confidence":0.9980469,"speaker":"A"},{"text":"through","start":1121910,"end":1122110,"confidence":1,"speaker":"A"},{"text":"CloudKit.","start":1122110,"end":1122910,"confidence":0.845459,"speaker":"A"}]},{"text":"So the idea today is we're going to talk about how to set something, how I set something like this up and how you could use use my library to then go ahead and do this yourself for any sort of work that you're going to do that where you want to use either a public or private database in CloudKit. So this is where I introduce myself. So I'm going to talk today about building Miskit, which is my library I built for doing CloudKit stuff on the server or essentially off of, not off of Apple platforms.","start":1125150,"end":1157140,"confidence":0.9873047,"words":[{"text":"So","start":1125150,"end":1125550,"confidence":0.9873047,"speaker":"A"},{"text":"the","start":1125630,"end":1125910,"confidence":0.99902344,"speaker":"A"},{"text":"idea","start":1125910,"end":1126270,"confidence":1,"speaker":"A"},{"text":"today","start":1126270,"end":1126550,"confidence":0.9995117,"speaker":"A"},{"text":"is","start":1126550,"end":1126790,"confidence":0.9980469,"speaker":"A"},{"text":"we're","start":1126790,"end":1127030,"confidence":0.9991862,"speaker":"A"},{"text":"going","start":1127030,"end":1127150,"confidence":0.88671875,"speaker":"A"},{"text":"to","start":1127150,"end":1127230,"confidence":1,"speaker":"A"},{"text":"talk","start":1127230,"end":1127390,"confidence":0.9995117,"speaker":"A"},{"text":"about","start":1127390,"end":1127710,"confidence":0.9975586,"speaker":"A"},{"text":"how","start":1128030,"end":1128350,"confidence":0.99365234,"speaker":"A"},{"text":"to","start":1128350,"end":1128550,"confidence":0.9707031,"speaker":"A"},{"text":"set","start":1128550,"end":1128750,"confidence":0.99853516,"speaker":"A"},{"text":"something,","start":1128750,"end":1129070,"confidence":0.95947266,"speaker":"A"},{"text":"how","start":1129070,"end":1129430,"confidence":0.9814453,"speaker":"A"},{"text":"I","start":1129430,"end":1129710,"confidence":0.99560547,"speaker":"A"},{"text":"set","start":1129710,"end":1129990,"confidence":0.99658203,"speaker":"A"},{"text":"something","start":1129990,"end":1130310,"confidence":1,"speaker":"A"},{"text":"like","start":1130310,"end":1130550,"confidence":0.99902344,"speaker":"A"},{"text":"this","start":1130550,"end":1130750,"confidence":0.9995117,"speaker":"A"},{"text":"up","start":1130750,"end":1131070,"confidence":0.99560547,"speaker":"A"},{"text":"and","start":1131860,"end":1132100,"confidence":0.9321289,"speaker":"A"},{"text":"how","start":1132100,"end":1132380,"confidence":0.9995117,"speaker":"A"},{"text":"you","start":1132380,"end":1132540,"confidence":0.99902344,"speaker":"A"},{"text":"could","start":1132540,"end":1132740,"confidence":0.99560547,"speaker":"A"},{"text":"use","start":1132740,"end":1133060,"confidence":0.9277344,"speaker":"A"},{"text":"use","start":1133300,"end":1133580,"confidence":1,"speaker":"A"},{"text":"my","start":1133580,"end":1133780,"confidence":0.99121094,"speaker":"A"},{"text":"library","start":1133780,"end":1134260,"confidence":0.9998372,"speaker":"A"},{"text":"to","start":1134260,"end":1134460,"confidence":0.9975586,"speaker":"A"},{"text":"then","start":1134460,"end":1134620,"confidence":0.9980469,"speaker":"A"},{"text":"go","start":1134620,"end":1134780,"confidence":0.99902344,"speaker":"A"},{"text":"ahead","start":1134780,"end":1134980,"confidence":0.9995117,"speaker":"A"},{"text":"and","start":1134980,"end":1135220,"confidence":0.53125,"speaker":"A"},{"text":"do","start":1135220,"end":1135420,"confidence":1,"speaker":"A"},{"text":"this","start":1135420,"end":1135620,"confidence":1,"speaker":"A"},{"text":"yourself","start":1135620,"end":1136060,"confidence":0.99975586,"speaker":"A"},{"text":"for","start":1136060,"end":1136340,"confidence":0.9995117,"speaker":"A"},{"text":"any","start":1136340,"end":1136660,"confidence":0.9995117,"speaker":"A"},{"text":"sort","start":1136660,"end":1136980,"confidence":0.9975586,"speaker":"A"},{"text":"of","start":1136980,"end":1137100,"confidence":0.9995117,"speaker":"A"},{"text":"work","start":1137100,"end":1137340,"confidence":0.9995117,"speaker":"A"},{"text":"that","start":1137340,"end":1137580,"confidence":0.99853516,"speaker":"A"},{"text":"you're","start":1137580,"end":1137780,"confidence":0.99886066,"speaker":"A"},{"text":"going","start":1137780,"end":1137860,"confidence":0.7861328,"speaker":"A"},{"text":"to","start":1137860,"end":1137940,"confidence":0.99853516,"speaker":"A"},{"text":"do","start":1137940,"end":1138060,"confidence":0.99902344,"speaker":"A"},{"text":"that","start":1138060,"end":1138260,"confidence":0.9140625,"speaker":"A"},{"text":"where","start":1138260,"end":1138460,"confidence":0.9970703,"speaker":"A"},{"text":"you","start":1138460,"end":1138580,"confidence":1,"speaker":"A"},{"text":"want","start":1138580,"end":1138700,"confidence":0.9140625,"speaker":"A"},{"text":"to","start":1138700,"end":1138860,"confidence":0.9941406,"speaker":"A"},{"text":"use","start":1138860,"end":1139100,"confidence":0.99609375,"speaker":"A"},{"text":"either","start":1139100,"end":1139420,"confidence":0.99975586,"speaker":"A"},{"text":"a","start":1139420,"end":1139580,"confidence":0.9238281,"speaker":"A"},{"text":"public","start":1139580,"end":1139780,"confidence":1,"speaker":"A"},{"text":"or","start":1139780,"end":1140020,"confidence":1,"speaker":"A"},{"text":"private","start":1140020,"end":1140300,"confidence":1,"speaker":"A"},{"text":"database","start":1140300,"end":1140980,"confidence":0.9995117,"speaker":"A"},{"text":"in","start":1141220,"end":1141500,"confidence":0.7890625,"speaker":"A"},{"text":"CloudKit.","start":1141500,"end":1142180,"confidence":0.99560547,"speaker":"A"},{"text":"So","start":1143300,"end":1143540,"confidence":0.9873047,"speaker":"A"},{"text":"this","start":1143540,"end":1143660,"confidence":0.9995117,"speaker":"A"},{"text":"is","start":1143660,"end":1143820,"confidence":1,"speaker":"A"},{"text":"where","start":1143820,"end":1143980,"confidence":0.9995117,"speaker":"A"},{"text":"I","start":1143980,"end":1144140,"confidence":0.97509766,"speaker":"A"},{"text":"introduce","start":1144140,"end":1144580,"confidence":0.96435547,"speaker":"A"},{"text":"myself.","start":1144580,"end":1145060,"confidence":0.99487305,"speaker":"A"},{"text":"So","start":1145940,"end":1146180,"confidence":0.9741211,"speaker":"A"},{"text":"I'm","start":1146180,"end":1146340,"confidence":0.99690753,"speaker":"A"},{"text":"going","start":1146340,"end":1146420,"confidence":0.9428711,"speaker":"A"},{"text":"to","start":1146420,"end":1146500,"confidence":0.99853516,"speaker":"A"},{"text":"talk","start":1146500,"end":1146660,"confidence":0.9995117,"speaker":"A"},{"text":"today","start":1146660,"end":1146860,"confidence":0.99121094,"speaker":"A"},{"text":"about","start":1146860,"end":1147020,"confidence":1,"speaker":"A"},{"text":"building","start":1147020,"end":1147299,"confidence":0.9995117,"speaker":"A"},{"text":"Miskit,","start":1147299,"end":1148020,"confidence":0.82421875,"speaker":"A"},{"text":"which","start":1148260,"end":1148540,"confidence":0.9995117,"speaker":"A"},{"text":"is","start":1148540,"end":1148700,"confidence":0.99072266,"speaker":"A"},{"text":"my","start":1148700,"end":1148860,"confidence":0.9995117,"speaker":"A"},{"text":"library","start":1148860,"end":1149300,"confidence":1,"speaker":"A"},{"text":"I","start":1149300,"end":1149500,"confidence":0.99853516,"speaker":"A"},{"text":"built","start":1149500,"end":1149860,"confidence":0.96761066,"speaker":"A"},{"text":"for","start":1150340,"end":1150700,"confidence":0.9921875,"speaker":"A"},{"text":"doing","start":1150700,"end":1151060,"confidence":0.9995117,"speaker":"A"},{"text":"CloudKit","start":1151460,"end":1152100,"confidence":0.99609375,"speaker":"A"},{"text":"stuff","start":1152100,"end":1152580,"confidence":0.99886066,"speaker":"A"},{"text":"on","start":1152740,"end":1153020,"confidence":0.94628906,"speaker":"A"},{"text":"the","start":1153020,"end":1153180,"confidence":0.9995117,"speaker":"A"},{"text":"server","start":1153180,"end":1153540,"confidence":1,"speaker":"A"},{"text":"or","start":1153540,"end":1153740,"confidence":0.9951172,"speaker":"A"},{"text":"essentially","start":1153740,"end":1154180,"confidence":0.9970703,"speaker":"A"},{"text":"off","start":1154180,"end":1154420,"confidence":0.8652344,"speaker":"A"},{"text":"of,","start":1154420,"end":1154740,"confidence":0.9970703,"speaker":"A"},{"text":"not","start":1155380,"end":1155660,"confidence":0.99853516,"speaker":"A"},{"text":"off","start":1155660,"end":1155860,"confidence":0.9995117,"speaker":"A"},{"text":"of","start":1155860,"end":1156100,"confidence":0.9970703,"speaker":"A"},{"text":"Apple","start":1156100,"end":1156500,"confidence":0.99975586,"speaker":"A"},{"text":"platforms.","start":1156500,"end":1157140,"confidence":0.9978841,"speaker":"A"}]},{"text":"Evan, do you have any questions before I keep going? No, it's good. Good topic though. So like I said, we have CloudKit Web Services and CloudKit Web Services. We provide a lot of documentation.","start":1159770,"end":1174210,"confidence":0.9189453,"words":[{"text":"Evan,","start":1159770,"end":1160050,"confidence":0.9189453,"speaker":"A"},{"text":"do","start":1160050,"end":1160170,"confidence":0.9980469,"speaker":"A"},{"text":"you","start":1160170,"end":1160250,"confidence":0.9873047,"speaker":"A"},{"text":"have","start":1160250,"end":1160330,"confidence":0.9995117,"speaker":"A"},{"text":"any","start":1160330,"end":1160450,"confidence":0.99902344,"speaker":"A"},{"text":"questions","start":1160450,"end":1160850,"confidence":0.99975586,"speaker":"A"},{"text":"before","start":1160850,"end":1161010,"confidence":1,"speaker":"A"},{"text":"I","start":1161010,"end":1161170,"confidence":0.99853516,"speaker":"A"},{"text":"keep","start":1161170,"end":1161330,"confidence":0.99902344,"speaker":"A"},{"text":"going?","start":1161330,"end":1161610,"confidence":0.99902344,"speaker":"A"},{"text":"No,","start":1162730,"end":1163130,"confidence":0.9770508,"speaker":"B"},{"text":"it's","start":1163370,"end":1163730,"confidence":0.9757487,"speaker":"B"},{"text":"good.","start":1163730,"end":1163970,"confidence":0.6723633,"speaker":"B"},{"text":"Good","start":1163970,"end":1164250,"confidence":1,"speaker":"B"},{"text":"topic","start":1164250,"end":1164610,"confidence":0.9953613,"speaker":"B"},{"text":"though.","start":1164610,"end":1164890,"confidence":0.99072266,"speaker":"B"},{"text":"So","start":1166810,"end":1167090,"confidence":0.9042969,"speaker":"A"},{"text":"like","start":1167090,"end":1167250,"confidence":0.9951172,"speaker":"A"},{"text":"I","start":1167250,"end":1167410,"confidence":1,"speaker":"A"},{"text":"said,","start":1167410,"end":1167610,"confidence":1,"speaker":"A"},{"text":"we","start":1167610,"end":1167810,"confidence":1,"speaker":"A"},{"text":"have","start":1167810,"end":1167970,"confidence":1,"speaker":"A"},{"text":"CloudKit","start":1167970,"end":1168570,"confidence":0.86804,"speaker":"A"},{"text":"Web","start":1168570,"end":1168810,"confidence":0.99853516,"speaker":"A"},{"text":"Services","start":1168810,"end":1169050,"confidence":0.99853516,"speaker":"A"},{"text":"and","start":1170170,"end":1170530,"confidence":0.8461914,"speaker":"A"},{"text":"CloudKit","start":1170530,"end":1171090,"confidence":0.9489746,"speaker":"A"},{"text":"Web","start":1171090,"end":1171330,"confidence":0.9975586,"speaker":"A"},{"text":"Services.","start":1171330,"end":1171610,"confidence":0.99902344,"speaker":"A"},{"text":"We","start":1172330,"end":1172730,"confidence":0.53759766,"speaker":"A"},{"text":"provide","start":1172730,"end":1173090,"confidence":1,"speaker":"A"},{"text":"a","start":1173090,"end":1173329,"confidence":0.96240234,"speaker":"A"},{"text":"lot","start":1173329,"end":1173489,"confidence":1,"speaker":"A"},{"text":"of","start":1173489,"end":1173610,"confidence":0.99853516,"speaker":"A"},{"text":"documentation.","start":1173610,"end":1174210,"confidence":0.99990237,"speaker":"A"}]},{"text":"We talked about CloudKit JS and the instructions on how to compose a web service request which has everything I need to compose one. And back in 2020 I did this all manually.","start":1174210,"end":1184570,"confidence":0.99902344,"words":[{"text":"We","start":1174210,"end":1174450,"confidence":0.99902344,"speaker":"A"},{"text":"talked","start":1174450,"end":1174650,"confidence":0.9987793,"speaker":"A"},{"text":"about","start":1174650,"end":1174770,"confidence":0.99902344,"speaker":"A"},{"text":"CloudKit","start":1174770,"end":1175330,"confidence":0.9980469,"speaker":"A"},{"text":"JS","start":1175330,"end":1175770,"confidence":0.7067871,"speaker":"A"},{"text":"and","start":1175850,"end":1176170,"confidence":0.9921875,"speaker":"A"},{"text":"the","start":1176170,"end":1176370,"confidence":0.9819336,"speaker":"A"},{"text":"instructions","start":1176370,"end":1176890,"confidence":0.9773763,"speaker":"A"},{"text":"on","start":1176890,"end":1177090,"confidence":0.9995117,"speaker":"A"},{"text":"how","start":1177090,"end":1177290,"confidence":0.9995117,"speaker":"A"},{"text":"to","start":1177290,"end":1177530,"confidence":0.9995117,"speaker":"A"},{"text":"compose","start":1177530,"end":1177930,"confidence":0.99853516,"speaker":"A"},{"text":"a","start":1177930,"end":1178090,"confidence":0.9926758,"speaker":"A"},{"text":"web","start":1178090,"end":1178410,"confidence":0.9980469,"speaker":"A"},{"text":"service","start":1178650,"end":1179050,"confidence":0.9902344,"speaker":"A"},{"text":"request","start":1179050,"end":1179570,"confidence":0.99853516,"speaker":"A"},{"text":"which","start":1179570,"end":1179810,"confidence":0.99902344,"speaker":"A"},{"text":"has","start":1179810,"end":1180090,"confidence":0.9975586,"speaker":"A"},{"text":"everything","start":1180090,"end":1180450,"confidence":0.9995117,"speaker":"A"},{"text":"I","start":1180450,"end":1180730,"confidence":0.9980469,"speaker":"A"},{"text":"need","start":1180730,"end":1181050,"confidence":0.99853516,"speaker":"A"},{"text":"to","start":1181210,"end":1181490,"confidence":0.99853516,"speaker":"A"},{"text":"compose","start":1181490,"end":1181810,"confidence":0.99487305,"speaker":"A"},{"text":"one.","start":1181810,"end":1182050,"confidence":0.57421875,"speaker":"A"},{"text":"And","start":1182050,"end":1182370,"confidence":0.81640625,"speaker":"A"},{"text":"back","start":1182370,"end":1182610,"confidence":1,"speaker":"A"},{"text":"in","start":1182610,"end":1182810,"confidence":0.9995117,"speaker":"A"},{"text":"2020","start":1182810,"end":1183370,"confidence":0.9978,"speaker":"A"},{"text":"I","start":1183370,"end":1183610,"confidence":0.9995117,"speaker":"A"},{"text":"did","start":1183610,"end":1183730,"confidence":0.99902344,"speaker":"A"},{"text":"this","start":1183730,"end":1183890,"confidence":0.98535156,"speaker":"A"},{"text":"all","start":1183890,"end":1184090,"confidence":0.99316406,"speaker":"A"},{"text":"manually.","start":1184090,"end":1184570,"confidence":0.9992676,"speaker":"A"}]},{"text":"The thing is at this point, if you look at right there, actually if you look at the top, you can see it hasn't been updated in over 10 years, which is kind of crazy, but it works. And then we got introduced to something back in WWDC I want to say it was 23.","start":1186600,"end":1208200,"confidence":0.9946289,"words":[{"text":"The","start":1186600,"end":1186760,"confidence":0.9946289,"speaker":"A"},{"text":"thing","start":1186760,"end":1187000,"confidence":0.9995117,"speaker":"A"},{"text":"is","start":1187000,"end":1187240,"confidence":0.99902344,"speaker":"A"},{"text":"at","start":1187240,"end":1187440,"confidence":0.99902344,"speaker":"A"},{"text":"this","start":1187440,"end":1187640,"confidence":0.9995117,"speaker":"A"},{"text":"point,","start":1187640,"end":1187960,"confidence":0.9995117,"speaker":"A"},{"text":"if","start":1188600,"end":1188880,"confidence":0.99902344,"speaker":"A"},{"text":"you","start":1188880,"end":1189040,"confidence":0.9995117,"speaker":"A"},{"text":"look","start":1189040,"end":1189200,"confidence":0.9995117,"speaker":"A"},{"text":"at","start":1189200,"end":1189440,"confidence":0.9814453,"speaker":"A"},{"text":"right","start":1189440,"end":1189720,"confidence":0.99902344,"speaker":"A"},{"text":"there,","start":1189720,"end":1190040,"confidence":0.99902344,"speaker":"A"},{"text":"actually","start":1191000,"end":1191320,"confidence":0.99316406,"speaker":"A"},{"text":"if","start":1191320,"end":1191480,"confidence":0.9995117,"speaker":"A"},{"text":"you","start":1191480,"end":1191560,"confidence":0.9995117,"speaker":"A"},{"text":"look","start":1191560,"end":1191680,"confidence":1,"speaker":"A"},{"text":"at","start":1191680,"end":1191800,"confidence":0.9995117,"speaker":"A"},{"text":"the","start":1191800,"end":1191920,"confidence":0.9995117,"speaker":"A"},{"text":"top,","start":1191920,"end":1192120,"confidence":0.9995117,"speaker":"A"},{"text":"you","start":1192120,"end":1192280,"confidence":0.9995117,"speaker":"A"},{"text":"can","start":1192280,"end":1192400,"confidence":1,"speaker":"A"},{"text":"see","start":1192400,"end":1192600,"confidence":0.9995117,"speaker":"A"},{"text":"it","start":1192600,"end":1192760,"confidence":0.98828125,"speaker":"A"},{"text":"hasn't","start":1192760,"end":1193080,"confidence":0.99768066,"speaker":"A"},{"text":"been","start":1193080,"end":1193200,"confidence":0.9995117,"speaker":"A"},{"text":"updated","start":1193200,"end":1193560,"confidence":0.99975586,"speaker":"A"},{"text":"in","start":1193560,"end":1193800,"confidence":0.96875,"speaker":"A"},{"text":"over","start":1193800,"end":1194120,"confidence":0.99902344,"speaker":"A"},{"text":"10","start":1194200,"end":1194480,"confidence":0.99951,"speaker":"A"},{"text":"years,","start":1194480,"end":1194760,"confidence":0.99902344,"speaker":"A"},{"text":"which","start":1196600,"end":1196880,"confidence":0.9975586,"speaker":"A"},{"text":"is","start":1196880,"end":1197160,"confidence":0.99853516,"speaker":"A"},{"text":"kind","start":1197160,"end":1197440,"confidence":0.88671875,"speaker":"A"},{"text":"of","start":1197440,"end":1197600,"confidence":0.9736328,"speaker":"A"},{"text":"crazy,","start":1197600,"end":1198120,"confidence":0.9996745,"speaker":"A"},{"text":"but","start":1198920,"end":1199200,"confidence":0.99609375,"speaker":"A"},{"text":"it","start":1199200,"end":1199360,"confidence":0.99902344,"speaker":"A"},{"text":"works.","start":1199360,"end":1199800,"confidence":0.99731445,"speaker":"A"},{"text":"And","start":1200999,"end":1201280,"confidence":0.7661133,"speaker":"A"},{"text":"then","start":1201280,"end":1201560,"confidence":0.9995117,"speaker":"A"},{"text":"we","start":1202040,"end":1202440,"confidence":0.9975586,"speaker":"A"},{"text":"got","start":1202840,"end":1203240,"confidence":0.96191406,"speaker":"A"},{"text":"introduced","start":1204200,"end":1204800,"confidence":0.9563802,"speaker":"A"},{"text":"to","start":1204800,"end":1204960,"confidence":0.9355469,"speaker":"A"},{"text":"something","start":1204960,"end":1205200,"confidence":0.9970703,"speaker":"A"},{"text":"back","start":1205200,"end":1205440,"confidence":0.9951172,"speaker":"A"},{"text":"in","start":1205440,"end":1205600,"confidence":0.9897461,"speaker":"A"},{"text":"WWDC","start":1205600,"end":1206520,"confidence":0.7050781,"speaker":"A"},{"text":"I","start":1206520,"end":1206760,"confidence":0.93896484,"speaker":"A"},{"text":"want","start":1206760,"end":1206840,"confidence":0.89404297,"speaker":"A"},{"text":"to","start":1206840,"end":1206920,"confidence":0.9980469,"speaker":"A"},{"text":"say","start":1206920,"end":1207040,"confidence":0.99609375,"speaker":"A"},{"text":"it","start":1207040,"end":1207160,"confidence":0.8076172,"speaker":"A"},{"text":"was","start":1207160,"end":1207400,"confidence":0.79248047,"speaker":"A"},{"text":"23.","start":1207480,"end":1208200,"confidence":0.99805,"speaker":"A"}]},{"text":"We got introduced to the Open API generator which is really nice because then we have, we can generate the Swift code if we know what the Open API documentation looks like it. And of course Apple doesn't provide one for CloudKit but they did provide a pretty big piece open. If you ever you looked at the Open API generator, it's amazing. Takes the Open API gamble file and generates all the Swift code you need. One of the other issues I had with first developing Miskit in 2020 was that there was no way to like there was no abstraction layer which could differentiate between doing something on the server or using regular like URL session which is more targeted towards client side.","start":1210280,"end":1256080,"confidence":0.99853516,"words":[{"text":"We","start":1210280,"end":1210600,"confidence":0.99853516,"speaker":"A"},{"text":"got","start":1210600,"end":1210840,"confidence":0.96240234,"speaker":"A"},{"text":"introduced","start":1210840,"end":1211360,"confidence":0.9744466,"speaker":"A"},{"text":"to","start":1211360,"end":1211520,"confidence":0.9995117,"speaker":"A"},{"text":"the","start":1211520,"end":1211680,"confidence":0.9995117,"speaker":"A"},{"text":"Open","start":1211680,"end":1211920,"confidence":0.9980469,"speaker":"A"},{"text":"API","start":1211920,"end":1212440,"confidence":0.97436523,"speaker":"A"},{"text":"generator","start":1212440,"end":1213000,"confidence":0.9851074,"speaker":"A"},{"text":"which","start":1213800,"end":1214000,"confidence":0.99365234,"speaker":"A"},{"text":"is","start":1214000,"end":1214320,"confidence":1,"speaker":"A"},{"text":"really","start":1214320,"end":1214600,"confidence":0.9995117,"speaker":"A"},{"text":"nice","start":1214600,"end":1215000,"confidence":1,"speaker":"A"},{"text":"because","start":1215000,"end":1215400,"confidence":0.99902344,"speaker":"A"},{"text":"then","start":1215960,"end":1216360,"confidence":0.9760742,"speaker":"A"},{"text":"we","start":1216840,"end":1217160,"confidence":0.6513672,"speaker":"A"},{"text":"have,","start":1217160,"end":1217480,"confidence":0.9902344,"speaker":"A"},{"text":"we","start":1217640,"end":1217920,"confidence":0.99609375,"speaker":"A"},{"text":"can","start":1217920,"end":1218080,"confidence":0.99902344,"speaker":"A"},{"text":"generate","start":1218080,"end":1218440,"confidence":0.99975586,"speaker":"A"},{"text":"the","start":1218440,"end":1218560,"confidence":0.9975586,"speaker":"A"},{"text":"Swift","start":1218560,"end":1218840,"confidence":0.7780762,"speaker":"A"},{"text":"code","start":1218840,"end":1219120,"confidence":0.96761066,"speaker":"A"},{"text":"if","start":1219120,"end":1219280,"confidence":1,"speaker":"A"},{"text":"we","start":1219280,"end":1219440,"confidence":0.99902344,"speaker":"A"},{"text":"know","start":1219440,"end":1219640,"confidence":0.98779297,"speaker":"A"},{"text":"what","start":1219640,"end":1219840,"confidence":0.99853516,"speaker":"A"},{"text":"the","start":1219840,"end":1220080,"confidence":0.9638672,"speaker":"A"},{"text":"Open","start":1220080,"end":1220400,"confidence":0.9980469,"speaker":"A"},{"text":"API","start":1220400,"end":1220880,"confidence":0.8979492,"speaker":"A"},{"text":"documentation","start":1220880,"end":1221720,"confidence":0.99970704,"speaker":"A"},{"text":"looks","start":1222200,"end":1222600,"confidence":1,"speaker":"A"},{"text":"like","start":1222600,"end":1222720,"confidence":0.99902344,"speaker":"A"},{"text":"it.","start":1222720,"end":1222880,"confidence":0.7519531,"speaker":"A"},{"text":"And","start":1222880,"end":1223040,"confidence":0.87597656,"speaker":"A"},{"text":"of","start":1223040,"end":1223160,"confidence":0.9980469,"speaker":"A"},{"text":"course","start":1223160,"end":1223280,"confidence":1,"speaker":"A"},{"text":"Apple","start":1223280,"end":1223600,"confidence":0.99975586,"speaker":"A"},{"text":"doesn't","start":1223600,"end":1223840,"confidence":0.99853516,"speaker":"A"},{"text":"provide","start":1223840,"end":1224080,"confidence":1,"speaker":"A"},{"text":"one","start":1224080,"end":1224320,"confidence":0.9926758,"speaker":"A"},{"text":"for","start":1224320,"end":1224480,"confidence":0.99902344,"speaker":"A"},{"text":"CloudKit","start":1224480,"end":1225240,"confidence":0.9314,"speaker":"A"},{"text":"but","start":1225960,"end":1226280,"confidence":0.9951172,"speaker":"A"},{"text":"they","start":1226280,"end":1226480,"confidence":0.88427734,"speaker":"A"},{"text":"did","start":1226480,"end":1226720,"confidence":0.98779297,"speaker":"A"},{"text":"provide","start":1226720,"end":1227040,"confidence":1,"speaker":"A"},{"text":"a","start":1227040,"end":1227280,"confidence":0.9995117,"speaker":"A"},{"text":"pretty","start":1227280,"end":1227520,"confidence":0.9998372,"speaker":"A"},{"text":"big","start":1227520,"end":1227720,"confidence":1,"speaker":"A"},{"text":"piece","start":1227720,"end":1228120,"confidence":0.99869794,"speaker":"A"},{"text":"open.","start":1229240,"end":1229639,"confidence":0.6689453,"speaker":"A"},{"text":"If","start":1229800,"end":1230040,"confidence":0.9873047,"speaker":"A"},{"text":"you","start":1230040,"end":1230120,"confidence":0.77490234,"speaker":"A"},{"text":"ever","start":1230120,"end":1230360,"confidence":0.91748047,"speaker":"A"},{"text":"you","start":1230360,"end":1230640,"confidence":0.7763672,"speaker":"A"},{"text":"looked","start":1230640,"end":1230920,"confidence":0.9987793,"speaker":"A"},{"text":"at","start":1230920,"end":1231000,"confidence":0.9995117,"speaker":"A"},{"text":"the","start":1231000,"end":1231120,"confidence":0.99902344,"speaker":"A"},{"text":"Open","start":1231120,"end":1231320,"confidence":0.99902344,"speaker":"A"},{"text":"API","start":1231320,"end":1231760,"confidence":0.9448242,"speaker":"A"},{"text":"generator,","start":1231760,"end":1232160,"confidence":0.9995117,"speaker":"A"},{"text":"it's","start":1232160,"end":1232400,"confidence":0.89192706,"speaker":"A"},{"text":"amazing.","start":1232400,"end":1232840,"confidence":0.9998372,"speaker":"A"},{"text":"Takes","start":1232840,"end":1233200,"confidence":0.7607422,"speaker":"A"},{"text":"the","start":1233200,"end":1233320,"confidence":0.46704102,"speaker":"A"},{"text":"Open","start":1233320,"end":1233520,"confidence":0.99902344,"speaker":"A"},{"text":"API","start":1233520,"end":1234080,"confidence":0.9501953,"speaker":"A"},{"text":"gamble","start":1234080,"end":1234640,"confidence":0.7845052,"speaker":"A"},{"text":"file","start":1234640,"end":1235000,"confidence":0.99121094,"speaker":"A"},{"text":"and","start":1235000,"end":1235320,"confidence":0.53125,"speaker":"A"},{"text":"generates","start":1235560,"end":1236160,"confidence":0.99975586,"speaker":"A"},{"text":"all","start":1236160,"end":1236400,"confidence":0.9995117,"speaker":"A"},{"text":"the","start":1236400,"end":1236560,"confidence":0.99609375,"speaker":"A"},{"text":"Swift","start":1236560,"end":1236840,"confidence":0.7429199,"speaker":"A"},{"text":"code","start":1236840,"end":1237080,"confidence":0.9991862,"speaker":"A"},{"text":"you","start":1237080,"end":1237240,"confidence":0.99853516,"speaker":"A"},{"text":"need.","start":1237240,"end":1237560,"confidence":1,"speaker":"A"},{"text":"One","start":1237880,"end":1238160,"confidence":0.99560547,"speaker":"A"},{"text":"of","start":1238160,"end":1238320,"confidence":0.9995117,"speaker":"A"},{"text":"the","start":1238320,"end":1238440,"confidence":1,"speaker":"A"},{"text":"other","start":1238440,"end":1238600,"confidence":0.99902344,"speaker":"A"},{"text":"issues","start":1238600,"end":1238880,"confidence":0.9995117,"speaker":"A"},{"text":"I","start":1238880,"end":1239120,"confidence":0.99902344,"speaker":"A"},{"text":"had","start":1239120,"end":1239280,"confidence":0.99658203,"speaker":"A"},{"text":"with","start":1239280,"end":1239560,"confidence":0.98828125,"speaker":"A"},{"text":"first","start":1240880,"end":1241040,"confidence":0.98339844,"speaker":"A"},{"text":"developing","start":1241040,"end":1241480,"confidence":0.99902344,"speaker":"A"},{"text":"Miskit","start":1241480,"end":1242160,"confidence":0.90844727,"speaker":"A"},{"text":"in","start":1242160,"end":1242440,"confidence":0.99072266,"speaker":"A"},{"text":"2020","start":1242440,"end":1243120,"confidence":0.99658,"speaker":"A"},{"text":"was","start":1243600,"end":1243920,"confidence":0.99609375,"speaker":"A"},{"text":"that","start":1243920,"end":1244160,"confidence":0.9951172,"speaker":"A"},{"text":"there","start":1244160,"end":1244360,"confidence":1,"speaker":"A"},{"text":"was","start":1244360,"end":1244520,"confidence":0.9995117,"speaker":"A"},{"text":"no","start":1244520,"end":1244720,"confidence":1,"speaker":"A"},{"text":"way","start":1244720,"end":1245000,"confidence":1,"speaker":"A"},{"text":"to","start":1245000,"end":1245320,"confidence":0.99658203,"speaker":"A"},{"text":"like","start":1245320,"end":1245680,"confidence":0.99072266,"speaker":"A"},{"text":"there","start":1245840,"end":1246160,"confidence":0.9770508,"speaker":"A"},{"text":"was","start":1246160,"end":1246360,"confidence":0.9941406,"speaker":"A"},{"text":"no","start":1246360,"end":1246520,"confidence":0.95410156,"speaker":"A"},{"text":"abstraction","start":1246520,"end":1247120,"confidence":0.9992676,"speaker":"A"},{"text":"layer","start":1247120,"end":1247520,"confidence":0.99934894,"speaker":"A"},{"text":"which","start":1247520,"end":1247800,"confidence":0.99902344,"speaker":"A"},{"text":"could","start":1247800,"end":1248040,"confidence":0.99316406,"speaker":"A"},{"text":"differentiate","start":1248040,"end":1248640,"confidence":0.9992676,"speaker":"A"},{"text":"between","start":1248640,"end":1248920,"confidence":1,"speaker":"A"},{"text":"doing","start":1248920,"end":1249200,"confidence":0.99902344,"speaker":"A"},{"text":"something","start":1249200,"end":1249440,"confidence":1,"speaker":"A"},{"text":"on","start":1249440,"end":1249640,"confidence":0.99902344,"speaker":"A"},{"text":"the","start":1249640,"end":1249800,"confidence":0.98876953,"speaker":"A"},{"text":"server","start":1249800,"end":1250320,"confidence":0.9995117,"speaker":"A"},{"text":"or","start":1250720,"end":1251080,"confidence":0.99902344,"speaker":"A"},{"text":"using","start":1251080,"end":1251440,"confidence":0.9975586,"speaker":"A"},{"text":"regular","start":1251760,"end":1252400,"confidence":0.99902344,"speaker":"A"},{"text":"like","start":1252480,"end":1252880,"confidence":0.9765625,"speaker":"A"},{"text":"URL","start":1253040,"end":1253680,"confidence":0.9951172,"speaker":"A"},{"text":"session","start":1253680,"end":1254040,"confidence":0.9991862,"speaker":"A"},{"text":"which","start":1254040,"end":1254200,"confidence":0.9995117,"speaker":"A"},{"text":"is","start":1254200,"end":1254360,"confidence":0.99658203,"speaker":"A"},{"text":"more","start":1254360,"end":1254600,"confidence":1,"speaker":"A"},{"text":"targeted","start":1254600,"end":1255080,"confidence":1,"speaker":"A"},{"text":"towards","start":1255080,"end":1255360,"confidence":0.9992676,"speaker":"A"},{"text":"client","start":1255360,"end":1255719,"confidence":0.9328613,"speaker":"A"},{"text":"side.","start":1255719,"end":1256080,"confidence":0.99853516,"speaker":"A"}]},{"text":"So I had to build my own abstraction for that. Luckily Open API has, there's open API transport I believe, which provides an abstraction layer where you can then plug in either use Async HTTP client, which is the server way of doing it, or you can plug in a URL session transport, which is of course the client way to do, provides a really great tutorial. I highly recommend checking this out as well as the doxy documentation that they provide. So this is great. But then I'd have to go ahead and I'd have to figure out a way to convert all this documentation into an open API document.","start":1258960,"end":1301140,"confidence":0.9970703,"words":[{"text":"So","start":1258960,"end":1259360,"confidence":0.9970703,"speaker":"A"},{"text":"I","start":1259440,"end":1259720,"confidence":0.99121094,"speaker":"A"},{"text":"had","start":1259720,"end":1259880,"confidence":0.8510742,"speaker":"A"},{"text":"to","start":1259880,"end":1260000,"confidence":0.97216797,"speaker":"A"},{"text":"build","start":1260000,"end":1260120,"confidence":0.9970703,"speaker":"A"},{"text":"my","start":1260120,"end":1260280,"confidence":0.9995117,"speaker":"A"},{"text":"own","start":1260280,"end":1260440,"confidence":1,"speaker":"A"},{"text":"abstraction","start":1260440,"end":1261000,"confidence":0.90441895,"speaker":"A"},{"text":"for","start":1261000,"end":1261120,"confidence":1,"speaker":"A"},{"text":"that.","start":1261120,"end":1261280,"confidence":1,"speaker":"A"},{"text":"Luckily","start":1261280,"end":1261640,"confidence":0.99641925,"speaker":"A"},{"text":"Open","start":1261640,"end":1261840,"confidence":0.9995117,"speaker":"A"},{"text":"API","start":1261840,"end":1262440,"confidence":0.7475586,"speaker":"A"},{"text":"has,","start":1262440,"end":1262800,"confidence":0.99609375,"speaker":"A"},{"text":"there's","start":1264080,"end":1264560,"confidence":0.99820966,"speaker":"A"},{"text":"open","start":1264560,"end":1264880,"confidence":0.87109375,"speaker":"A"},{"text":"API","start":1264960,"end":1265600,"confidence":0.8029785,"speaker":"A"},{"text":"transport","start":1265600,"end":1266240,"confidence":0.99902344,"speaker":"A"},{"text":"I","start":1266240,"end":1266520,"confidence":0.99658203,"speaker":"A"},{"text":"believe,","start":1266520,"end":1266800,"confidence":0.9995117,"speaker":"A"},{"text":"which","start":1266880,"end":1267240,"confidence":0.9995117,"speaker":"A"},{"text":"provides","start":1267240,"end":1267600,"confidence":0.9995117,"speaker":"A"},{"text":"an","start":1267600,"end":1267720,"confidence":0.99121094,"speaker":"A"},{"text":"abstraction","start":1267720,"end":1268400,"confidence":0.98132324,"speaker":"A"},{"text":"layer","start":1268480,"end":1268840,"confidence":0.96940106,"speaker":"A"},{"text":"where","start":1268840,"end":1269000,"confidence":0.9995117,"speaker":"A"},{"text":"you","start":1269000,"end":1269120,"confidence":1,"speaker":"A"},{"text":"can","start":1269120,"end":1269240,"confidence":0.9995117,"speaker":"A"},{"text":"then","start":1269240,"end":1269400,"confidence":0.9975586,"speaker":"A"},{"text":"plug","start":1269400,"end":1269640,"confidence":0.9992676,"speaker":"A"},{"text":"in","start":1269640,"end":1269840,"confidence":0.9946289,"speaker":"A"},{"text":"either","start":1269840,"end":1270120,"confidence":0.9980469,"speaker":"A"},{"text":"use","start":1270120,"end":1270400,"confidence":0.99316406,"speaker":"A"},{"text":"Async","start":1270980,"end":1271420,"confidence":0.94433594,"speaker":"A"},{"text":"HTTP","start":1271420,"end":1272100,"confidence":0.9790039,"speaker":"A"},{"text":"client,","start":1272100,"end":1272620,"confidence":0.9975586,"speaker":"A"},{"text":"which","start":1272620,"end":1272900,"confidence":0.9995117,"speaker":"A"},{"text":"is","start":1272900,"end":1273140,"confidence":0.9995117,"speaker":"A"},{"text":"the","start":1273140,"end":1273420,"confidence":0.9995117,"speaker":"A"},{"text":"server","start":1273420,"end":1273900,"confidence":0.99902344,"speaker":"A"},{"text":"way","start":1273900,"end":1274060,"confidence":0.98583984,"speaker":"A"},{"text":"of","start":1274060,"end":1274220,"confidence":1,"speaker":"A"},{"text":"doing","start":1274220,"end":1274380,"confidence":1,"speaker":"A"},{"text":"it,","start":1274380,"end":1274540,"confidence":0.9995117,"speaker":"A"},{"text":"or","start":1274540,"end":1274780,"confidence":0.59228516,"speaker":"A"},{"text":"you","start":1274780,"end":1275020,"confidence":0.9995117,"speaker":"A"},{"text":"can","start":1275020,"end":1275180,"confidence":0.9995117,"speaker":"A"},{"text":"plug","start":1275180,"end":1275380,"confidence":0.99975586,"speaker":"A"},{"text":"in","start":1275380,"end":1275500,"confidence":0.99658203,"speaker":"A"},{"text":"a","start":1275500,"end":1275660,"confidence":0.99609375,"speaker":"A"},{"text":"URL","start":1275660,"end":1276180,"confidence":0.99853516,"speaker":"A"},{"text":"session","start":1276180,"end":1276660,"confidence":0.87906903,"speaker":"A"},{"text":"transport,","start":1277060,"end":1277780,"confidence":0.99902344,"speaker":"A"},{"text":"which","start":1277860,"end":1278180,"confidence":0.9995117,"speaker":"A"},{"text":"is","start":1278180,"end":1278500,"confidence":0.9995117,"speaker":"A"},{"text":"of","start":1278500,"end":1278780,"confidence":0.5307617,"speaker":"A"},{"text":"course","start":1278780,"end":1278940,"confidence":0.9995117,"speaker":"A"},{"text":"the","start":1278940,"end":1279100,"confidence":0.5600586,"speaker":"A"},{"text":"client","start":1279100,"end":1279380,"confidence":0.99487305,"speaker":"A"},{"text":"way","start":1279380,"end":1279580,"confidence":0.9941406,"speaker":"A"},{"text":"to","start":1279580,"end":1279700,"confidence":0.9995117,"speaker":"A"},{"text":"do,","start":1279700,"end":1279820,"confidence":0.9995117,"speaker":"A"},{"text":"provides","start":1282060,"end":1282420,"confidence":0.9995117,"speaker":"A"},{"text":"a","start":1282420,"end":1282540,"confidence":0.9995117,"speaker":"A"},{"text":"really","start":1282540,"end":1282700,"confidence":0.9995117,"speaker":"A"},{"text":"great","start":1282700,"end":1282980,"confidence":0.9995117,"speaker":"A"},{"text":"tutorial.","start":1283060,"end":1283740,"confidence":0.9855957,"speaker":"A"},{"text":"I","start":1283740,"end":1283980,"confidence":0.96777344,"speaker":"A"},{"text":"highly","start":1283980,"end":1284300,"confidence":0.998291,"speaker":"A"},{"text":"recommend","start":1284300,"end":1284620,"confidence":1,"speaker":"A"},{"text":"checking","start":1284620,"end":1284900,"confidence":0.99934894,"speaker":"A"},{"text":"this","start":1284900,"end":1285060,"confidence":0.9951172,"speaker":"A"},{"text":"out","start":1285060,"end":1285380,"confidence":0.9970703,"speaker":"A"},{"text":"as","start":1286579,"end":1286859,"confidence":1,"speaker":"A"},{"text":"well","start":1286859,"end":1287020,"confidence":1,"speaker":"A"},{"text":"as","start":1287020,"end":1287300,"confidence":0.99853516,"speaker":"A"},{"text":"the","start":1287380,"end":1287740,"confidence":0.9975586,"speaker":"A"},{"text":"doxy","start":1287740,"end":1288340,"confidence":0.84684247,"speaker":"A"},{"text":"documentation","start":1288340,"end":1289060,"confidence":0.99990237,"speaker":"A"},{"text":"that","start":1289220,"end":1289500,"confidence":0.99853516,"speaker":"A"},{"text":"they","start":1289500,"end":1289700,"confidence":0.9995117,"speaker":"A"},{"text":"provide.","start":1289700,"end":1290020,"confidence":0.9970703,"speaker":"A"},{"text":"So","start":1291860,"end":1292220,"confidence":0.9667969,"speaker":"A"},{"text":"this","start":1292220,"end":1292460,"confidence":0.9995117,"speaker":"A"},{"text":"is","start":1292460,"end":1292660,"confidence":0.95654297,"speaker":"A"},{"text":"great.","start":1292660,"end":1292940,"confidence":1,"speaker":"A"},{"text":"But","start":1292940,"end":1293180,"confidence":0.99609375,"speaker":"A"},{"text":"then","start":1293180,"end":1293420,"confidence":0.99853516,"speaker":"A"},{"text":"I'd","start":1293420,"end":1293820,"confidence":0.99625653,"speaker":"A"},{"text":"have","start":1293820,"end":1293980,"confidence":0.9995117,"speaker":"A"},{"text":"to","start":1293980,"end":1294100,"confidence":1,"speaker":"A"},{"text":"go","start":1294100,"end":1294220,"confidence":0.9995117,"speaker":"A"},{"text":"ahead","start":1294220,"end":1294500,"confidence":0.9995117,"speaker":"A"},{"text":"and","start":1294660,"end":1294940,"confidence":0.99853516,"speaker":"A"},{"text":"I'd","start":1294940,"end":1295180,"confidence":0.8806966,"speaker":"A"},{"text":"have","start":1295180,"end":1295300,"confidence":0.9995117,"speaker":"A"},{"text":"to","start":1295300,"end":1295420,"confidence":0.9995117,"speaker":"A"},{"text":"figure","start":1295420,"end":1295660,"confidence":0.7961426,"speaker":"A"},{"text":"out","start":1295660,"end":1295820,"confidence":0.99853516,"speaker":"A"},{"text":"a","start":1295820,"end":1295980,"confidence":0.9970703,"speaker":"A"},{"text":"way","start":1295980,"end":1296260,"confidence":0.99560547,"speaker":"A"},{"text":"to","start":1296900,"end":1297020,"confidence":0.9819336,"speaker":"A"},{"text":"convert","start":1297020,"end":1297300,"confidence":0.9992676,"speaker":"A"},{"text":"all","start":1297300,"end":1297540,"confidence":0.9995117,"speaker":"A"},{"text":"this","start":1297540,"end":1297740,"confidence":0.9975586,"speaker":"A"},{"text":"documentation","start":1297740,"end":1298500,"confidence":0.9995117,"speaker":"A"},{"text":"into","start":1298660,"end":1299060,"confidence":0.9995117,"speaker":"A"},{"text":"an","start":1299140,"end":1299420,"confidence":0.99853516,"speaker":"A"},{"text":"open","start":1299420,"end":1299700,"confidence":0.9995117,"speaker":"A"},{"text":"API","start":1299700,"end":1300340,"confidence":0.9458008,"speaker":"A"},{"text":"document.","start":1300420,"end":1301140,"confidence":0.9998779,"speaker":"A"}]},{"text":"I mean, can you guess what helped me to get build an open API document from all this documentation? Some of the tools, some AI tool. Yes. AI came and I'm like, holy crap. Like AI is really good at documenting your code, but it's also pretty darn good at taking documentation and building code.","start":1302420,"end":1326250,"confidence":0.5463867,"words":[{"text":"I","start":1302420,"end":1302700,"confidence":0.5463867,"speaker":"A"},{"text":"mean,","start":1302700,"end":1302860,"confidence":0.9926758,"speaker":"A"},{"text":"can","start":1302860,"end":1303020,"confidence":0.9995117,"speaker":"A"},{"text":"you","start":1303020,"end":1303180,"confidence":0.99902344,"speaker":"A"},{"text":"guess","start":1303180,"end":1303540,"confidence":0.99975586,"speaker":"A"},{"text":"what","start":1303940,"end":1304260,"confidence":0.9995117,"speaker":"A"},{"text":"helped","start":1304260,"end":1304620,"confidence":0.76538086,"speaker":"A"},{"text":"me","start":1304620,"end":1304980,"confidence":0.9926758,"speaker":"A"},{"text":"to","start":1305540,"end":1305820,"confidence":0.9873047,"speaker":"A"},{"text":"get","start":1305820,"end":1306100,"confidence":0.6230469,"speaker":"A"},{"text":"build","start":1306180,"end":1306580,"confidence":0.95996094,"speaker":"A"},{"text":"an","start":1306820,"end":1307100,"confidence":0.9550781,"speaker":"A"},{"text":"open","start":1307100,"end":1307340,"confidence":0.9995117,"speaker":"A"},{"text":"API","start":1307340,"end":1307860,"confidence":0.90722656,"speaker":"A"},{"text":"document","start":1307860,"end":1308260,"confidence":0.9959717,"speaker":"A"},{"text":"from","start":1308260,"end":1308460,"confidence":0.99853516,"speaker":"A"},{"text":"all","start":1308460,"end":1308620,"confidence":0.9995117,"speaker":"A"},{"text":"this","start":1308620,"end":1308820,"confidence":0.9555664,"speaker":"A"},{"text":"documentation?","start":1308820,"end":1309540,"confidence":0.9988281,"speaker":"A"},{"text":"Some","start":1310340,"end":1310740,"confidence":0.62402344,"speaker":"B"},{"text":"of","start":1311060,"end":1311260,"confidence":0.25683594,"speaker":"B"},{"text":"the","start":1311260,"end":1311300,"confidence":0.56347656,"speaker":"B"},{"text":"tools,","start":1311300,"end":1311620,"confidence":0.72314453,"speaker":"B"},{"text":"some","start":1312659,"end":1312940,"confidence":0.9658203,"speaker":"B"},{"text":"AI","start":1312940,"end":1313260,"confidence":0.9914551,"speaker":"B"},{"text":"tool.","start":1313260,"end":1313540,"confidence":0.9716797,"speaker":"B"},{"text":"Yes.","start":1314500,"end":1314980,"confidence":0.9482422,"speaker":"A"},{"text":"AI","start":1316820,"end":1317340,"confidence":0.91967773,"speaker":"A"},{"text":"came","start":1317340,"end":1317620,"confidence":0.9980469,"speaker":"A"},{"text":"and","start":1317620,"end":1317900,"confidence":0.99853516,"speaker":"A"},{"text":"I'm","start":1317900,"end":1318140,"confidence":0.99934894,"speaker":"A"},{"text":"like,","start":1318140,"end":1318340,"confidence":0.9921875,"speaker":"A"},{"text":"holy","start":1318340,"end":1318620,"confidence":0.82543945,"speaker":"A"},{"text":"crap.","start":1318620,"end":1318980,"confidence":0.86450195,"speaker":"A"},{"text":"Like","start":1319460,"end":1319860,"confidence":0.6220703,"speaker":"A"},{"text":"AI","start":1320180,"end":1320660,"confidence":0.92407227,"speaker":"A"},{"text":"is","start":1320660,"end":1320860,"confidence":0.9946289,"speaker":"A"},{"text":"really","start":1320860,"end":1321020,"confidence":0.99902344,"speaker":"A"},{"text":"good","start":1321020,"end":1321180,"confidence":0.99902344,"speaker":"A"},{"text":"at","start":1321180,"end":1321340,"confidence":0.9995117,"speaker":"A"},{"text":"documenting","start":1321340,"end":1321820,"confidence":0.99990237,"speaker":"A"},{"text":"your","start":1321820,"end":1321980,"confidence":0.99902344,"speaker":"A"},{"text":"code,","start":1321980,"end":1322260,"confidence":0.9998372,"speaker":"A"},{"text":"but","start":1322260,"end":1322460,"confidence":0.96972656,"speaker":"A"},{"text":"it's","start":1322460,"end":1322660,"confidence":0.9749349,"speaker":"A"},{"text":"also","start":1322660,"end":1322820,"confidence":0.9995117,"speaker":"A"},{"text":"pretty","start":1322820,"end":1323060,"confidence":0.9996745,"speaker":"A"},{"text":"darn","start":1323060,"end":1323260,"confidence":0.90804034,"speaker":"A"},{"text":"good","start":1323260,"end":1323420,"confidence":1,"speaker":"A"},{"text":"at","start":1323420,"end":1323700,"confidence":0.9902344,"speaker":"A"},{"text":"taking","start":1324490,"end":1324690,"confidence":0.93066406,"speaker":"A"},{"text":"documentation","start":1324690,"end":1325370,"confidence":0.9998047,"speaker":"A"},{"text":"and","start":1325370,"end":1325570,"confidence":0.99609375,"speaker":"A"},{"text":"building","start":1325570,"end":1325810,"confidence":0.9995117,"speaker":"A"},{"text":"code.","start":1325810,"end":1326250,"confidence":0.8733724,"speaker":"A"}]},{"text":"So then I would just plug it. I've been plugging in with Claude and it has a copy of all the documentation in my repo and it can go ahead and edit the open API. It's not perfect by any means, of course, but that's what unit tests are for.","start":1326890,"end":1341610,"confidence":0.9238281,"words":[{"text":"So","start":1326890,"end":1327170,"confidence":0.9238281,"speaker":"A"},{"text":"then","start":1327170,"end":1327450,"confidence":0.99658203,"speaker":"A"},{"text":"I","start":1327930,"end":1328250,"confidence":0.9819336,"speaker":"A"},{"text":"would","start":1328250,"end":1328450,"confidence":0.9848633,"speaker":"A"},{"text":"just","start":1328450,"end":1328610,"confidence":0.99902344,"speaker":"A"},{"text":"plug","start":1328610,"end":1328850,"confidence":0.9938965,"speaker":"A"},{"text":"it.","start":1328850,"end":1329050,"confidence":0.8227539,"speaker":"A"},{"text":"I've","start":1329050,"end":1329290,"confidence":0.99397784,"speaker":"A"},{"text":"been","start":1329290,"end":1329410,"confidence":0.9975586,"speaker":"A"},{"text":"plugging","start":1329410,"end":1329730,"confidence":0.95751953,"speaker":"A"},{"text":"in","start":1329730,"end":1329890,"confidence":0.8691406,"speaker":"A"},{"text":"with","start":1329890,"end":1330050,"confidence":0.9995117,"speaker":"A"},{"text":"Claude","start":1330050,"end":1330650,"confidence":0.73999023,"speaker":"A"},{"text":"and","start":1331050,"end":1331330,"confidence":0.9667969,"speaker":"A"},{"text":"it","start":1331330,"end":1331490,"confidence":0.9975586,"speaker":"A"},{"text":"has","start":1331490,"end":1331650,"confidence":1,"speaker":"A"},{"text":"a","start":1331650,"end":1331850,"confidence":0.9995117,"speaker":"A"},{"text":"copy","start":1331850,"end":1332170,"confidence":1,"speaker":"A"},{"text":"of","start":1332170,"end":1332290,"confidence":1,"speaker":"A"},{"text":"all","start":1332290,"end":1332450,"confidence":0.9995117,"speaker":"A"},{"text":"the","start":1332450,"end":1332610,"confidence":0.9995117,"speaker":"A"},{"text":"documentation","start":1332610,"end":1333210,"confidence":0.99970704,"speaker":"A"},{"text":"in","start":1333210,"end":1333410,"confidence":0.9277344,"speaker":"A"},{"text":"my","start":1333410,"end":1333570,"confidence":1,"speaker":"A"},{"text":"repo","start":1333570,"end":1334090,"confidence":0.9848633,"speaker":"A"},{"text":"and","start":1334410,"end":1334730,"confidence":0.9682617,"speaker":"A"},{"text":"it","start":1334730,"end":1334930,"confidence":0.8828125,"speaker":"A"},{"text":"can","start":1334930,"end":1335090,"confidence":0.9995117,"speaker":"A"},{"text":"go","start":1335090,"end":1335250,"confidence":0.9995117,"speaker":"A"},{"text":"ahead","start":1335250,"end":1335410,"confidence":0.9995117,"speaker":"A"},{"text":"and","start":1335410,"end":1335610,"confidence":0.99853516,"speaker":"A"},{"text":"edit","start":1335610,"end":1336090,"confidence":0.9995117,"speaker":"A"},{"text":"the","start":1336250,"end":1336490,"confidence":0.9824219,"speaker":"A"},{"text":"open","start":1336490,"end":1336690,"confidence":0.99316406,"speaker":"A"},{"text":"API.","start":1336690,"end":1337210,"confidence":0.9802246,"speaker":"A"},{"text":"It's","start":1337210,"end":1337490,"confidence":0.9817708,"speaker":"A"},{"text":"not","start":1337490,"end":1337690,"confidence":0.99853516,"speaker":"A"},{"text":"perfect","start":1337690,"end":1338010,"confidence":0.97998047,"speaker":"A"},{"text":"by","start":1338010,"end":1338250,"confidence":0.99853516,"speaker":"A"},{"text":"any","start":1338250,"end":1338490,"confidence":1,"speaker":"A"},{"text":"means,","start":1338490,"end":1338810,"confidence":1,"speaker":"A"},{"text":"of","start":1338810,"end":1339090,"confidence":0.99902344,"speaker":"A"},{"text":"course,","start":1339090,"end":1339370,"confidence":1,"speaker":"A"},{"text":"but","start":1339530,"end":1339849,"confidence":0.9970703,"speaker":"A"},{"text":"that's","start":1339849,"end":1340170,"confidence":0.9998372,"speaker":"A"},{"text":"what","start":1340170,"end":1340410,"confidence":0.9980469,"speaker":"A"},{"text":"unit","start":1340410,"end":1340850,"confidence":0.84521484,"speaker":"A"},{"text":"tests","start":1340850,"end":1341210,"confidence":0.9946289,"speaker":"A"},{"text":"are","start":1341210,"end":1341330,"confidence":0.99560547,"speaker":"A"},{"text":"for.","start":1341330,"end":1341610,"confidence":0.99658203,"speaker":"A"}]},{"text":"And actually having integration tests in order to do stuff so that.","start":1343850,"end":1351700,"confidence":0.89697266,"words":[{"text":"And","start":1343850,"end":1344170,"confidence":0.89697266,"speaker":"A"},{"text":"actually","start":1344170,"end":1344410,"confidence":0.99853516,"speaker":"A"},{"text":"having","start":1344410,"end":1344650,"confidence":0.87402344,"speaker":"A"},{"text":"integration","start":1344650,"end":1345210,"confidence":0.9769287,"speaker":"A"},{"text":"tests","start":1345210,"end":1345770,"confidence":0.9975586,"speaker":"A"},{"text":"in","start":1346250,"end":1346530,"confidence":0.99853516,"speaker":"A"},{"text":"order","start":1346530,"end":1346730,"confidence":1,"speaker":"A"},{"text":"to","start":1346730,"end":1346930,"confidence":0.9995117,"speaker":"A"},{"text":"do","start":1346930,"end":1347130,"confidence":0.9995117,"speaker":"A"},{"text":"stuff","start":1347130,"end":1347530,"confidence":0.9998372,"speaker":"A"},{"text":"so","start":1347690,"end":1348090,"confidence":0.83496094,"speaker":"A"},{"text":"that.","start":1351460,"end":1351700,"confidence":0.9980469,"speaker":"A"}]},{"text":"Sorry, I just want to make sure nothing important.","start":1355380,"end":1361460,"confidence":0.9995117,"words":[{"text":"Sorry,","start":1355380,"end":1355740,"confidence":0.9995117,"speaker":"A"},{"text":"I","start":1355740,"end":1355860,"confidence":0.9995117,"speaker":"A"},{"text":"just","start":1355860,"end":1355980,"confidence":1,"speaker":"A"},{"text":"want","start":1355980,"end":1356140,"confidence":0.99560547,"speaker":"A"},{"text":"to","start":1356140,"end":1356300,"confidence":0.99365234,"speaker":"A"},{"text":"make","start":1356300,"end":1356460,"confidence":1,"speaker":"A"},{"text":"sure","start":1356460,"end":1356740,"confidence":1,"speaker":"A"},{"text":"nothing","start":1360660,"end":1361100,"confidence":0.88623047,"speaker":"A"},{"text":"important.","start":1361100,"end":1361460,"confidence":1,"speaker":"A"}]},{"text":"I hate teams.","start":1366900,"end":1368020,"confidence":0.9951172,"words":[{"text":"I","start":1366900,"end":1367180,"confidence":0.9951172,"speaker":"A"},{"text":"hate","start":1367180,"end":1367460,"confidence":0.9992676,"speaker":"A"},{"text":"teams.","start":1367460,"end":1368020,"confidence":0.9995117,"speaker":"A"}]},{"text":"Okay, so great. So let's talk about.","start":1373060,"end":1376420,"confidence":0.94677734,"words":[{"text":"Okay,","start":1373060,"end":1373620,"confidence":0.94677734,"speaker":"A"},{"text":"so","start":1374820,"end":1375100,"confidence":0.9980469,"speaker":"A"},{"text":"great.","start":1375100,"end":1375380,"confidence":0.9980469,"speaker":"A"},{"text":"So","start":1375700,"end":1375780,"confidence":0.9995117,"speaker":"A"},{"text":"let's","start":1375780,"end":1375980,"confidence":0.9996745,"speaker":"A"},{"text":"talk","start":1375980,"end":1376140,"confidence":0.9995117,"speaker":"A"},{"text":"about.","start":1376140,"end":1376420,"confidence":0.9980469,"speaker":"A"}]},{"text":"Sorry, slides are still not done, but let's talk about authentication methods. You can see I have the logos here, but I haven't quite cleaned this up. So there's really two and a half authentication methods when it comes to CloudKit. So here is the miss demo database. You just go in here and you can go to tokens and keys and then that will give you access to set up either the API if you want to do API key or API token if you want to do a private database or a server to server keyset if you want to do a public database.","start":1379700,"end":1420190,"confidence":0.90966797,"words":[{"text":"Sorry,","start":1379700,"end":1380180,"confidence":0.90966797,"speaker":"A"},{"text":"slides","start":1380500,"end":1380900,"confidence":0.76538086,"speaker":"A"},{"text":"are","start":1380900,"end":1381100,"confidence":0.9995117,"speaker":"A"},{"text":"still","start":1381100,"end":1381260,"confidence":1,"speaker":"A"},{"text":"not","start":1381260,"end":1381420,"confidence":1,"speaker":"A"},{"text":"done,","start":1381420,"end":1381620,"confidence":0.9980469,"speaker":"A"},{"text":"but","start":1381620,"end":1381940,"confidence":0.99316406,"speaker":"A"},{"text":"let's","start":1382100,"end":1382460,"confidence":0.9991862,"speaker":"A"},{"text":"talk","start":1382460,"end":1382620,"confidence":0.9995117,"speaker":"A"},{"text":"about","start":1382620,"end":1382900,"confidence":0.9980469,"speaker":"A"},{"text":"authentication","start":1384500,"end":1385380,"confidence":1,"speaker":"A"},{"text":"methods.","start":1385380,"end":1386020,"confidence":0.99975586,"speaker":"A"},{"text":"You","start":1386340,"end":1386620,"confidence":0.9970703,"speaker":"A"},{"text":"can","start":1386620,"end":1386780,"confidence":0.8959961,"speaker":"A"},{"text":"see","start":1386780,"end":1386940,"confidence":0.9995117,"speaker":"A"},{"text":"I","start":1386940,"end":1387100,"confidence":0.99902344,"speaker":"A"},{"text":"have","start":1387100,"end":1387380,"confidence":0.9995117,"speaker":"A"},{"text":"the","start":1387460,"end":1387740,"confidence":0.99121094,"speaker":"A"},{"text":"logos","start":1387740,"end":1388140,"confidence":0.9980469,"speaker":"A"},{"text":"here,","start":1388140,"end":1388300,"confidence":0.9995117,"speaker":"A"},{"text":"but","start":1388300,"end":1388420,"confidence":1,"speaker":"A"},{"text":"I","start":1388420,"end":1388540,"confidence":0.9995117,"speaker":"A"},{"text":"haven't","start":1388540,"end":1388780,"confidence":0.99975586,"speaker":"A"},{"text":"quite","start":1388780,"end":1389020,"confidence":0.99975586,"speaker":"A"},{"text":"cleaned","start":1389020,"end":1389340,"confidence":0.79541016,"speaker":"A"},{"text":"this","start":1389340,"end":1389540,"confidence":0.9941406,"speaker":"A"},{"text":"up.","start":1389540,"end":1389860,"confidence":0.9970703,"speaker":"A"},{"text":"So","start":1390820,"end":1391220,"confidence":0.9770508,"speaker":"A"},{"text":"there's","start":1391940,"end":1392540,"confidence":0.9983724,"speaker":"A"},{"text":"really","start":1392540,"end":1392900,"confidence":0.99902344,"speaker":"A"},{"text":"two","start":1393780,"end":1394140,"confidence":1,"speaker":"A"},{"text":"and","start":1394140,"end":1394380,"confidence":0.87890625,"speaker":"A"},{"text":"a","start":1394380,"end":1394540,"confidence":0.9667969,"speaker":"A"},{"text":"half","start":1394540,"end":1394820,"confidence":0.9995117,"speaker":"A"},{"text":"authentication","start":1394820,"end":1395660,"confidence":0.99975586,"speaker":"A"},{"text":"methods","start":1395660,"end":1396140,"confidence":1,"speaker":"A"},{"text":"when","start":1396140,"end":1396300,"confidence":1,"speaker":"A"},{"text":"it","start":1396300,"end":1396420,"confidence":1,"speaker":"A"},{"text":"comes","start":1396420,"end":1396540,"confidence":1,"speaker":"A"},{"text":"to","start":1396540,"end":1396700,"confidence":1,"speaker":"A"},{"text":"CloudKit.","start":1396700,"end":1397380,"confidence":0.9552,"speaker":"A"},{"text":"So","start":1398420,"end":1398820,"confidence":0.9326172,"speaker":"A"},{"text":"here","start":1398900,"end":1399300,"confidence":0.99853516,"speaker":"A"},{"text":"is","start":1399460,"end":1399860,"confidence":0.9658203,"speaker":"A"},{"text":"the","start":1401150,"end":1401270,"confidence":0.95947266,"speaker":"A"},{"text":"miss","start":1401270,"end":1401470,"confidence":0.5654297,"speaker":"A"},{"text":"demo","start":1401470,"end":1401950,"confidence":0.7548828,"speaker":"A"},{"text":"database.","start":1401950,"end":1402630,"confidence":0.9996745,"speaker":"A"},{"text":"You","start":1402630,"end":1402870,"confidence":0.9995117,"speaker":"A"},{"text":"just","start":1402870,"end":1403030,"confidence":0.9995117,"speaker":"A"},{"text":"go","start":1403030,"end":1403230,"confidence":0.9995117,"speaker":"A"},{"text":"in","start":1403230,"end":1403430,"confidence":0.9995117,"speaker":"A"},{"text":"here","start":1403430,"end":1403710,"confidence":0.9995117,"speaker":"A"},{"text":"and","start":1404270,"end":1404550,"confidence":0.99560547,"speaker":"A"},{"text":"you","start":1404550,"end":1404710,"confidence":0.99853516,"speaker":"A"},{"text":"can","start":1404710,"end":1404870,"confidence":0.99365234,"speaker":"A"},{"text":"go","start":1404870,"end":1404990,"confidence":1,"speaker":"A"},{"text":"to","start":1404990,"end":1405110,"confidence":0.9995117,"speaker":"A"},{"text":"tokens","start":1405110,"end":1405510,"confidence":0.99975586,"speaker":"A"},{"text":"and","start":1405510,"end":1405670,"confidence":0.9892578,"speaker":"A"},{"text":"keys","start":1405670,"end":1406070,"confidence":0.9992676,"speaker":"A"},{"text":"and","start":1406070,"end":1406310,"confidence":0.9975586,"speaker":"A"},{"text":"then","start":1406310,"end":1406470,"confidence":0.99902344,"speaker":"A"},{"text":"that","start":1406470,"end":1406630,"confidence":0.9995117,"speaker":"A"},{"text":"will","start":1406630,"end":1406790,"confidence":0.9995117,"speaker":"A"},{"text":"give","start":1406790,"end":1406950,"confidence":1,"speaker":"A"},{"text":"you","start":1406950,"end":1407150,"confidence":1,"speaker":"A"},{"text":"access","start":1407150,"end":1407470,"confidence":1,"speaker":"A"},{"text":"to","start":1407470,"end":1407750,"confidence":0.98339844,"speaker":"A"},{"text":"set","start":1407750,"end":1407950,"confidence":0.99658203,"speaker":"A"},{"text":"up","start":1407950,"end":1408270,"confidence":0.7631836,"speaker":"A"},{"text":"either","start":1408510,"end":1408990,"confidence":0.99975586,"speaker":"A"},{"text":"the","start":1408990,"end":1409390,"confidence":0.9995117,"speaker":"A"},{"text":"API","start":1409870,"end":1410550,"confidence":0.9995117,"speaker":"A"},{"text":"if","start":1410550,"end":1410750,"confidence":0.9995117,"speaker":"A"},{"text":"you","start":1410750,"end":1410870,"confidence":0.9243164,"speaker":"A"},{"text":"want","start":1410870,"end":1411030,"confidence":0.94921875,"speaker":"A"},{"text":"to","start":1411030,"end":1411150,"confidence":0.9980469,"speaker":"A"},{"text":"do","start":1411150,"end":1411390,"confidence":0.9970703,"speaker":"A"},{"text":"API","start":1411790,"end":1412430,"confidence":0.9926758,"speaker":"A"},{"text":"key","start":1412430,"end":1412830,"confidence":0.9995117,"speaker":"A"},{"text":"or","start":1412830,"end":1413110,"confidence":0.9980469,"speaker":"A"},{"text":"API","start":1413110,"end":1413470,"confidence":0.8027344,"speaker":"A"},{"text":"token","start":1413470,"end":1414030,"confidence":0.86376953,"speaker":"A"},{"text":"if","start":1414270,"end":1414550,"confidence":0.9995117,"speaker":"A"},{"text":"you","start":1414550,"end":1414710,"confidence":1,"speaker":"A"},{"text":"want","start":1414710,"end":1414830,"confidence":0.9394531,"speaker":"A"},{"text":"to","start":1414830,"end":1414910,"confidence":0.99902344,"speaker":"A"},{"text":"do","start":1414910,"end":1415070,"confidence":0.99902344,"speaker":"A"},{"text":"a","start":1415070,"end":1415270,"confidence":0.53125,"speaker":"A"},{"text":"private","start":1415270,"end":1415470,"confidence":1,"speaker":"A"},{"text":"database","start":1415470,"end":1416190,"confidence":0.9998372,"speaker":"A"},{"text":"or","start":1416190,"end":1416550,"confidence":0.9995117,"speaker":"A"},{"text":"a","start":1416550,"end":1416790,"confidence":0.99853516,"speaker":"A"},{"text":"server","start":1416790,"end":1417109,"confidence":0.9946289,"speaker":"A"},{"text":"to","start":1417109,"end":1417310,"confidence":0.97753906,"speaker":"A"},{"text":"server","start":1417310,"end":1417630,"confidence":0.9992676,"speaker":"A"},{"text":"keyset","start":1417630,"end":1418190,"confidence":0.8388672,"speaker":"A"},{"text":"if","start":1418350,"end":1418630,"confidence":0.9995117,"speaker":"A"},{"text":"you","start":1418630,"end":1418750,"confidence":0.99902344,"speaker":"A"},{"text":"want","start":1418750,"end":1418870,"confidence":0.53808594,"speaker":"A"},{"text":"to","start":1418870,"end":1418990,"confidence":0.9951172,"speaker":"A"},{"text":"do","start":1418990,"end":1419150,"confidence":0.99902344,"speaker":"A"},{"text":"a","start":1419150,"end":1419310,"confidence":0.8515625,"speaker":"A"},{"text":"public","start":1419310,"end":1419470,"confidence":1,"speaker":"A"},{"text":"database.","start":1419470,"end":1420190,"confidence":0.9996745,"speaker":"A"}]},{"text":"So let's talk about the API token. Pretty simple. You just go into here, click the plus sign, you say a name and you say whether you want to do a post message or URL redirect. We'll get into that in a little bit in the next section. And then whether you want to have user info and you click save and you'll get a nice little API token you could use in your web your web calls essentially.","start":1420190,"end":1446680,"confidence":0.98095703,"words":[{"text":"So","start":1420190,"end":1420430,"confidence":0.98095703,"speaker":"A"},{"text":"let's","start":1420430,"end":1420590,"confidence":0.9998372,"speaker":"A"},{"text":"talk","start":1420590,"end":1420710,"confidence":0.99902344,"speaker":"A"},{"text":"about","start":1420710,"end":1420870,"confidence":0.99902344,"speaker":"A"},{"text":"the","start":1420870,"end":1421030,"confidence":0.9980469,"speaker":"A"},{"text":"API","start":1421030,"end":1421430,"confidence":0.99902344,"speaker":"A"},{"text":"token.","start":1421430,"end":1421950,"confidence":0.9773763,"speaker":"A"},{"text":"Pretty","start":1422510,"end":1422870,"confidence":1,"speaker":"A"},{"text":"simple.","start":1422870,"end":1423310,"confidence":0.83935547,"speaker":"A"},{"text":"You","start":1423470,"end":1423750,"confidence":0.9995117,"speaker":"A"},{"text":"just","start":1423750,"end":1423870,"confidence":1,"speaker":"A"},{"text":"go","start":1423870,"end":1423990,"confidence":0.99609375,"speaker":"A"},{"text":"into","start":1423990,"end":1424190,"confidence":0.61572266,"speaker":"A"},{"text":"here,","start":1424190,"end":1424510,"confidence":0.9995117,"speaker":"A"},{"text":"click","start":1424750,"end":1425110,"confidence":0.9987793,"speaker":"A"},{"text":"the","start":1425110,"end":1425270,"confidence":0.9995117,"speaker":"A"},{"text":"plus","start":1425270,"end":1425550,"confidence":0.9980469,"speaker":"A"},{"text":"sign,","start":1425550,"end":1425870,"confidence":0.9980469,"speaker":"A"},{"text":"you","start":1426840,"end":1427000,"confidence":0.9980469,"speaker":"A"},{"text":"say","start":1427000,"end":1427200,"confidence":0.99853516,"speaker":"A"},{"text":"a","start":1427200,"end":1427320,"confidence":0.91064453,"speaker":"A"},{"text":"name","start":1427320,"end":1427560,"confidence":0.99609375,"speaker":"A"},{"text":"and","start":1428600,"end":1428920,"confidence":0.9975586,"speaker":"A"},{"text":"you","start":1428920,"end":1429120,"confidence":0.99902344,"speaker":"A"},{"text":"say","start":1429120,"end":1429280,"confidence":0.9980469,"speaker":"A"},{"text":"whether","start":1429280,"end":1429440,"confidence":0.9995117,"speaker":"A"},{"text":"you","start":1429440,"end":1429600,"confidence":1,"speaker":"A"},{"text":"want","start":1429600,"end":1429720,"confidence":0.99560547,"speaker":"A"},{"text":"to","start":1429720,"end":1429800,"confidence":0.99560547,"speaker":"A"},{"text":"do","start":1429800,"end":1429920,"confidence":0.99853516,"speaker":"A"},{"text":"a","start":1429920,"end":1430040,"confidence":0.9995117,"speaker":"A"},{"text":"post","start":1430040,"end":1430240,"confidence":0.9995117,"speaker":"A"},{"text":"message","start":1430240,"end":1430680,"confidence":0.99902344,"speaker":"A"},{"text":"or","start":1430680,"end":1430920,"confidence":0.9995117,"speaker":"A"},{"text":"URL","start":1430920,"end":1431440,"confidence":0.8330078,"speaker":"A"},{"text":"redirect.","start":1431440,"end":1432040,"confidence":1,"speaker":"A"},{"text":"We'll","start":1432280,"end":1432640,"confidence":0.9708659,"speaker":"A"},{"text":"get","start":1432640,"end":1432800,"confidence":1,"speaker":"A"},{"text":"into","start":1432800,"end":1432960,"confidence":1,"speaker":"A"},{"text":"that","start":1432960,"end":1433120,"confidence":1,"speaker":"A"},{"text":"in","start":1433120,"end":1433280,"confidence":0.8725586,"speaker":"A"},{"text":"a","start":1433280,"end":1433400,"confidence":0.99902344,"speaker":"A"},{"text":"little","start":1433400,"end":1433560,"confidence":0.9526367,"speaker":"A"},{"text":"bit","start":1433560,"end":1433760,"confidence":1,"speaker":"A"},{"text":"in","start":1433760,"end":1433920,"confidence":0.99609375,"speaker":"A"},{"text":"the","start":1433920,"end":1434040,"confidence":0.9995117,"speaker":"A"},{"text":"next","start":1434040,"end":1434200,"confidence":0.9995117,"speaker":"A"},{"text":"section.","start":1434200,"end":1434680,"confidence":0.9995117,"speaker":"A"},{"text":"And","start":1435960,"end":1436240,"confidence":0.98828125,"speaker":"A"},{"text":"then","start":1436240,"end":1436480,"confidence":0.89453125,"speaker":"A"},{"text":"whether","start":1436480,"end":1436760,"confidence":0.9995117,"speaker":"A"},{"text":"you","start":1436760,"end":1436960,"confidence":1,"speaker":"A"},{"text":"want","start":1436960,"end":1437120,"confidence":0.9975586,"speaker":"A"},{"text":"to","start":1437120,"end":1437280,"confidence":1,"speaker":"A"},{"text":"have","start":1437280,"end":1437560,"confidence":1,"speaker":"A"},{"text":"user","start":1437800,"end":1438280,"confidence":0.99902344,"speaker":"A"},{"text":"info","start":1438280,"end":1438760,"confidence":1,"speaker":"A"},{"text":"and","start":1438840,"end":1439240,"confidence":0.99609375,"speaker":"A"},{"text":"you","start":1439400,"end":1439720,"confidence":0.99609375,"speaker":"A"},{"text":"click","start":1439720,"end":1440040,"confidence":0.9995117,"speaker":"A"},{"text":"save","start":1440040,"end":1440360,"confidence":0.9987793,"speaker":"A"},{"text":"and","start":1440360,"end":1440640,"confidence":0.9326172,"speaker":"A"},{"text":"you'll","start":1440640,"end":1440920,"confidence":0.99934894,"speaker":"A"},{"text":"get","start":1440920,"end":1441040,"confidence":1,"speaker":"A"},{"text":"a","start":1441040,"end":1441160,"confidence":0.9995117,"speaker":"A"},{"text":"nice","start":1441160,"end":1441400,"confidence":0.99975586,"speaker":"A"},{"text":"little","start":1441400,"end":1441680,"confidence":0.9995117,"speaker":"A"},{"text":"API","start":1441680,"end":1442280,"confidence":0.86499023,"speaker":"A"},{"text":"token","start":1442519,"end":1442960,"confidence":0.9996745,"speaker":"A"},{"text":"you","start":1442960,"end":1443120,"confidence":0.9995117,"speaker":"A"},{"text":"could","start":1443120,"end":1443280,"confidence":0.9951172,"speaker":"A"},{"text":"use","start":1443280,"end":1443520,"confidence":1,"speaker":"A"},{"text":"in","start":1443520,"end":1443760,"confidence":0.99658203,"speaker":"A"},{"text":"your","start":1443760,"end":1444040,"confidence":0.9848633,"speaker":"A"},{"text":"web","start":1444120,"end":1444600,"confidence":0.99560547,"speaker":"A"},{"text":"your","start":1445240,"end":1445560,"confidence":0.9873047,"speaker":"A"},{"text":"web","start":1445560,"end":1445840,"confidence":0.9987793,"speaker":"A"},{"text":"calls","start":1445840,"end":1446160,"confidence":0.9831543,"speaker":"A"},{"text":"essentially.","start":1446160,"end":1446680,"confidence":0.9581299,"speaker":"A"}]},{"text":"API doesn't really. The API token doesn't really give you a lot of. But what it does give you is it gives you an entry to get a web authentication token for a user. So basically the way that works. So you'll notice here, when we were in this section, we have this piece here called Sign in Callback.","start":1449000,"end":1469610,"confidence":0.8713379,"words":[{"text":"API","start":1449000,"end":1449560,"confidence":0.8713379,"speaker":"A"},{"text":"doesn't","start":1449560,"end":1449800,"confidence":0.99886066,"speaker":"A"},{"text":"really.","start":1449800,"end":1450000,"confidence":0.9980469,"speaker":"A"},{"text":"The","start":1450000,"end":1450200,"confidence":0.88720703,"speaker":"A"},{"text":"API","start":1450200,"end":1450640,"confidence":0.954834,"speaker":"A"},{"text":"token","start":1450640,"end":1451000,"confidence":0.99934894,"speaker":"A"},{"text":"doesn't","start":1451000,"end":1451200,"confidence":0.9160156,"speaker":"A"},{"text":"really","start":1451200,"end":1451360,"confidence":0.9995117,"speaker":"A"},{"text":"give","start":1451360,"end":1451520,"confidence":1,"speaker":"A"},{"text":"you","start":1451520,"end":1451680,"confidence":0.99853516,"speaker":"A"},{"text":"a","start":1451680,"end":1451800,"confidence":0.99853516,"speaker":"A"},{"text":"lot","start":1451800,"end":1452040,"confidence":0.99560547,"speaker":"A"},{"text":"of.","start":1452100,"end":1452260,"confidence":0.515625,"speaker":"A"},{"text":"But","start":1452570,"end":1452690,"confidence":0.98535156,"speaker":"A"},{"text":"what","start":1452690,"end":1452850,"confidence":0.99658203,"speaker":"A"},{"text":"it","start":1452850,"end":1452970,"confidence":0.9902344,"speaker":"A"},{"text":"does","start":1452970,"end":1453130,"confidence":0.9980469,"speaker":"A"},{"text":"give","start":1453130,"end":1453290,"confidence":0.99853516,"speaker":"A"},{"text":"you","start":1453290,"end":1453410,"confidence":0.99853516,"speaker":"A"},{"text":"is","start":1453410,"end":1453570,"confidence":0.98779297,"speaker":"A"},{"text":"it","start":1453570,"end":1453690,"confidence":0.9951172,"speaker":"A"},{"text":"gives","start":1453690,"end":1453890,"confidence":0.9733887,"speaker":"A"},{"text":"you","start":1453890,"end":1454010,"confidence":1,"speaker":"A"},{"text":"an","start":1454010,"end":1454170,"confidence":1,"speaker":"A"},{"text":"entry","start":1454170,"end":1454530,"confidence":0.99975586,"speaker":"A"},{"text":"to","start":1454530,"end":1454850,"confidence":1,"speaker":"A"},{"text":"get","start":1454850,"end":1455130,"confidence":0.9995117,"speaker":"A"},{"text":"a","start":1455130,"end":1455330,"confidence":0.9995117,"speaker":"A"},{"text":"web","start":1455330,"end":1455570,"confidence":1,"speaker":"A"},{"text":"authentication","start":1455570,"end":1456250,"confidence":0.8823242,"speaker":"A"},{"text":"token","start":1456250,"end":1456610,"confidence":0.9998372,"speaker":"A"},{"text":"for","start":1456610,"end":1456770,"confidence":0.9995117,"speaker":"A"},{"text":"a","start":1456770,"end":1456930,"confidence":0.48901367,"speaker":"A"},{"text":"user.","start":1456930,"end":1457450,"confidence":0.99902344,"speaker":"A"},{"text":"So","start":1457850,"end":1458130,"confidence":0.99121094,"speaker":"A"},{"text":"basically","start":1458130,"end":1458570,"confidence":0.99853516,"speaker":"A"},{"text":"the","start":1458730,"end":1459010,"confidence":1,"speaker":"A"},{"text":"way","start":1459010,"end":1459210,"confidence":0.9995117,"speaker":"A"},{"text":"that","start":1459210,"end":1459450,"confidence":1,"speaker":"A"},{"text":"works.","start":1459450,"end":1459930,"confidence":0.99731445,"speaker":"A"},{"text":"So","start":1460970,"end":1461370,"confidence":0.9580078,"speaker":"A"},{"text":"you'll","start":1461450,"end":1461810,"confidence":0.93896484,"speaker":"A"},{"text":"notice","start":1461810,"end":1462170,"confidence":0.99975586,"speaker":"A"},{"text":"here,","start":1462170,"end":1462490,"confidence":0.99902344,"speaker":"A"},{"text":"when","start":1463050,"end":1463370,"confidence":0.9941406,"speaker":"A"},{"text":"we","start":1463370,"end":1463570,"confidence":0.9995117,"speaker":"A"},{"text":"were","start":1463570,"end":1463770,"confidence":0.99853516,"speaker":"A"},{"text":"in","start":1463770,"end":1463970,"confidence":1,"speaker":"A"},{"text":"this","start":1463970,"end":1464250,"confidence":0.9995117,"speaker":"A"},{"text":"section,","start":1464330,"end":1464890,"confidence":0.99975586,"speaker":"A"},{"text":"we","start":1467050,"end":1467330,"confidence":0.9980469,"speaker":"A"},{"text":"have","start":1467330,"end":1467490,"confidence":0.9995117,"speaker":"A"},{"text":"this","start":1467490,"end":1467690,"confidence":1,"speaker":"A"},{"text":"piece","start":1467690,"end":1467970,"confidence":0.9998372,"speaker":"A"},{"text":"here","start":1467970,"end":1468250,"confidence":0.99902344,"speaker":"A"},{"text":"called","start":1468250,"end":1468569,"confidence":0.99902344,"speaker":"A"},{"text":"Sign","start":1468569,"end":1468770,"confidence":0.9926758,"speaker":"A"},{"text":"in","start":1468770,"end":1468970,"confidence":0.48339844,"speaker":"A"},{"text":"Callback.","start":1468970,"end":1469610,"confidence":0.9967448,"speaker":"A"}]},{"text":"So you can have either call a JavaScript, it's called a message event, it will call a Message event and a message event will have the metadata with the web authentication token of that user. Or you could do URL redirect where on authentication the user has a URL and then part of that URL is then having part of one of the query parameters and we'll get into that. We'll then have the web authentication token in the URL. So you put, basically you have your website, you add the JavaScript, you need to add the sign in with Apple. Oh, here's Josh.","start":1469770,"end":1508010,"confidence":0.9580078,"words":[{"text":"So","start":1469770,"end":1470170,"confidence":0.9580078,"speaker":"A"},{"text":"you","start":1470330,"end":1470650,"confidence":0.9995117,"speaker":"A"},{"text":"can","start":1470650,"end":1470930,"confidence":0.9995117,"speaker":"A"},{"text":"have","start":1470930,"end":1471250,"confidence":0.98291016,"speaker":"A"},{"text":"either","start":1471250,"end":1471690,"confidence":1,"speaker":"A"},{"text":"call","start":1471690,"end":1472010,"confidence":0.9741211,"speaker":"A"},{"text":"a","start":1472010,"end":1472210,"confidence":0.96875,"speaker":"A"},{"text":"JavaScript,","start":1472210,"end":1472970,"confidence":0.9967448,"speaker":"A"},{"text":"it's","start":1473370,"end":1473730,"confidence":0.99593097,"speaker":"A"},{"text":"called","start":1473730,"end":1473930,"confidence":0.9995117,"speaker":"A"},{"text":"a","start":1473930,"end":1474130,"confidence":0.9794922,"speaker":"A"},{"text":"message","start":1474130,"end":1474530,"confidence":0.9980469,"speaker":"A"},{"text":"event,","start":1474530,"end":1474810,"confidence":0.9897461,"speaker":"A"},{"text":"it","start":1475610,"end":1475890,"confidence":0.9941406,"speaker":"A"},{"text":"will","start":1475890,"end":1476090,"confidence":0.82177734,"speaker":"A"},{"text":"call","start":1476090,"end":1476330,"confidence":0.6923828,"speaker":"A"},{"text":"a","start":1476330,"end":1476530,"confidence":0.90625,"speaker":"A"},{"text":"Message","start":1476530,"end":1476850,"confidence":0.99902344,"speaker":"A"},{"text":"event","start":1476850,"end":1477090,"confidence":0.9897461,"speaker":"A"},{"text":"and","start":1477090,"end":1477450,"confidence":0.97265625,"speaker":"A"},{"text":"a","start":1477450,"end":1477730,"confidence":0.8847656,"speaker":"A"},{"text":"message","start":1477730,"end":1478050,"confidence":0.9987793,"speaker":"A"},{"text":"event","start":1478050,"end":1478250,"confidence":0.9951172,"speaker":"A"},{"text":"will","start":1478250,"end":1478450,"confidence":0.9921875,"speaker":"A"},{"text":"have","start":1478450,"end":1478610,"confidence":1,"speaker":"A"},{"text":"the","start":1478610,"end":1478730,"confidence":0.9975586,"speaker":"A"},{"text":"metadata","start":1478730,"end":1479250,"confidence":0.99886066,"speaker":"A"},{"text":"with","start":1479250,"end":1479410,"confidence":0.9995117,"speaker":"A"},{"text":"the","start":1479410,"end":1479530,"confidence":0.99560547,"speaker":"A"},{"text":"web","start":1479530,"end":1479730,"confidence":0.9992676,"speaker":"A"},{"text":"authentication","start":1479730,"end":1480410,"confidence":0.99975586,"speaker":"A"},{"text":"token","start":1480410,"end":1480770,"confidence":0.9998372,"speaker":"A"},{"text":"of","start":1480770,"end":1480930,"confidence":0.99902344,"speaker":"A"},{"text":"that","start":1480930,"end":1481090,"confidence":0.99902344,"speaker":"A"},{"text":"user.","start":1481090,"end":1481530,"confidence":0.99902344,"speaker":"A"},{"text":"Or","start":1482410,"end":1482530,"confidence":0.9902344,"speaker":"A"},{"text":"you","start":1482530,"end":1482650,"confidence":0.7363281,"speaker":"A"},{"text":"could","start":1482650,"end":1482770,"confidence":0.99072266,"speaker":"A"},{"text":"do","start":1482770,"end":1482930,"confidence":0.9946289,"speaker":"A"},{"text":"URL","start":1482930,"end":1483450,"confidence":0.99658203,"speaker":"A"},{"text":"redirect","start":1483450,"end":1484090,"confidence":0.99975586,"speaker":"A"},{"text":"where","start":1484170,"end":1484570,"confidence":0.99121094,"speaker":"A"},{"text":"on","start":1484810,"end":1485210,"confidence":0.8457031,"speaker":"A"},{"text":"authentication","start":1485290,"end":1486050,"confidence":0.99975586,"speaker":"A"},{"text":"the","start":1486050,"end":1486290,"confidence":0.9975586,"speaker":"A"},{"text":"user","start":1486290,"end":1486730,"confidence":0.99975586,"speaker":"A"},{"text":"has","start":1486970,"end":1487250,"confidence":0.99902344,"speaker":"A"},{"text":"a","start":1487250,"end":1487410,"confidence":0.9975586,"speaker":"A"},{"text":"URL","start":1487410,"end":1487930,"confidence":0.998291,"speaker":"A"},{"text":"and","start":1487930,"end":1488130,"confidence":0.99609375,"speaker":"A"},{"text":"then","start":1488130,"end":1488290,"confidence":0.9560547,"speaker":"A"},{"text":"part","start":1488290,"end":1488450,"confidence":1,"speaker":"A"},{"text":"of","start":1488450,"end":1488570,"confidence":1,"speaker":"A"},{"text":"that","start":1488570,"end":1488690,"confidence":0.9995117,"speaker":"A"},{"text":"URL","start":1488690,"end":1489170,"confidence":0.9980469,"speaker":"A"},{"text":"is","start":1489170,"end":1489330,"confidence":0.99609375,"speaker":"A"},{"text":"then","start":1489330,"end":1489530,"confidence":0.98291016,"speaker":"A"},{"text":"having","start":1489530,"end":1489850,"confidence":0.99658203,"speaker":"A"},{"text":"part","start":1490650,"end":1490930,"confidence":0.9921875,"speaker":"A"},{"text":"of","start":1490930,"end":1491090,"confidence":0.99853516,"speaker":"A"},{"text":"one","start":1491090,"end":1491210,"confidence":0.9995117,"speaker":"A"},{"text":"of","start":1491210,"end":1491290,"confidence":0.9995117,"speaker":"A"},{"text":"the","start":1491290,"end":1491370,"confidence":1,"speaker":"A"},{"text":"query","start":1491370,"end":1491690,"confidence":0.8486328,"speaker":"A"},{"text":"parameters","start":1491770,"end":1492570,"confidence":0.8824463,"speaker":"A"},{"text":"and","start":1492570,"end":1492850,"confidence":0.9814453,"speaker":"A"},{"text":"we'll","start":1492850,"end":1493050,"confidence":0.99934894,"speaker":"A"},{"text":"get","start":1493050,"end":1493130,"confidence":1,"speaker":"A"},{"text":"into","start":1493130,"end":1493290,"confidence":0.99902344,"speaker":"A"},{"text":"that.","start":1493290,"end":1493610,"confidence":0.9975586,"speaker":"A"},{"text":"We'll","start":1494250,"end":1494570,"confidence":0.89176434,"speaker":"A"},{"text":"then","start":1494570,"end":1494690,"confidence":0.9980469,"speaker":"A"},{"text":"have","start":1494690,"end":1494850,"confidence":1,"speaker":"A"},{"text":"the","start":1494850,"end":1495010,"confidence":0.9980469,"speaker":"A"},{"text":"web","start":1495010,"end":1495250,"confidence":0.9904785,"speaker":"A"},{"text":"authentication","start":1495250,"end":1495810,"confidence":0.9975586,"speaker":"A"},{"text":"token","start":1495810,"end":1496130,"confidence":0.9996745,"speaker":"A"},{"text":"in","start":1496130,"end":1496290,"confidence":0.99560547,"speaker":"A"},{"text":"the","start":1496290,"end":1496450,"confidence":1,"speaker":"A"},{"text":"URL.","start":1496450,"end":1497050,"confidence":0.99731445,"speaker":"A"},{"text":"So","start":1498570,"end":1498970,"confidence":0.9921875,"speaker":"A"},{"text":"you","start":1499050,"end":1499330,"confidence":0.9794922,"speaker":"A"},{"text":"put,","start":1499330,"end":1499610,"confidence":0.9970703,"speaker":"A"},{"text":"basically","start":1500010,"end":1500410,"confidence":0.9995117,"speaker":"A"},{"text":"you","start":1500410,"end":1500570,"confidence":0.71972656,"speaker":"A"},{"text":"have","start":1500570,"end":1500690,"confidence":0.99853516,"speaker":"A"},{"text":"your","start":1500690,"end":1500850,"confidence":1,"speaker":"A"},{"text":"website,","start":1500850,"end":1501130,"confidence":0.9980469,"speaker":"A"},{"text":"you","start":1501450,"end":1501850,"confidence":0.9995117,"speaker":"A"},{"text":"add","start":1501850,"end":1502130,"confidence":0.9980469,"speaker":"A"},{"text":"the","start":1502130,"end":1502290,"confidence":0.9995117,"speaker":"A"},{"text":"JavaScript,","start":1502290,"end":1503050,"confidence":0.9950358,"speaker":"A"},{"text":"you","start":1503210,"end":1503490,"confidence":0.99658203,"speaker":"A"},{"text":"need","start":1503490,"end":1503770,"confidence":0.9995117,"speaker":"A"},{"text":"to","start":1504330,"end":1504730,"confidence":0.99902344,"speaker":"A"},{"text":"add","start":1504970,"end":1505330,"confidence":0.9892578,"speaker":"A"},{"text":"the","start":1505330,"end":1505570,"confidence":0.9975586,"speaker":"A"},{"text":"sign","start":1505570,"end":1505770,"confidence":0.99902344,"speaker":"A"},{"text":"in","start":1505770,"end":1505970,"confidence":0.99609375,"speaker":"A"},{"text":"with","start":1505970,"end":1506170,"confidence":1,"speaker":"A"},{"text":"Apple.","start":1506170,"end":1506650,"confidence":0.9987793,"speaker":"A"},{"text":"Oh,","start":1506970,"end":1507330,"confidence":0.8078613,"speaker":"A"},{"text":"here's","start":1507330,"end":1507650,"confidence":0.9991862,"speaker":"A"},{"text":"Josh.","start":1507650,"end":1508010,"confidence":0.9987793,"speaker":"A"}]},{"text":"Oh cool. Josh, you there?","start":1514310,"end":1515910,"confidence":0.9213867,"words":[{"text":"Oh","start":1514310,"end":1514510,"confidence":0.9213867,"speaker":"A"},{"text":"cool.","start":1514510,"end":1514870,"confidence":0.99902344,"speaker":"A"},{"text":"Josh,","start":1514870,"end":1515350,"confidence":0.9995117,"speaker":"A"},{"text":"you","start":1515350,"end":1515590,"confidence":0.97265625,"speaker":"A"},{"text":"there?","start":1515590,"end":1515910,"confidence":0.9995117,"speaker":"A"}]},{"text":"I hope so. Good. Okay. Hey, we were just talking about how to set up. I'm going to go back a little bit Evan, but not too far back.","start":1518790,"end":1526630,"confidence":0.99853516,"words":[{"text":"I","start":1518790,"end":1519110,"confidence":0.99853516,"speaker":"C"},{"text":"hope","start":1519110,"end":1519390,"confidence":1,"speaker":"C"},{"text":"so.","start":1519390,"end":1519750,"confidence":0.99902344,"speaker":"C"},{"text":"Good.","start":1520710,"end":1521070,"confidence":0.9868164,"speaker":"A"},{"text":"Okay.","start":1521070,"end":1521590,"confidence":0.97753906,"speaker":"A"},{"text":"Hey,","start":1521750,"end":1522110,"confidence":0.9992676,"speaker":"A"},{"text":"we","start":1522110,"end":1522230,"confidence":0.99902344,"speaker":"A"},{"text":"were","start":1522230,"end":1522350,"confidence":0.51660156,"speaker":"A"},{"text":"just","start":1522350,"end":1522510,"confidence":1,"speaker":"A"},{"text":"talking","start":1522510,"end":1522750,"confidence":0.99975586,"speaker":"A"},{"text":"about","start":1522750,"end":1522990,"confidence":0.9970703,"speaker":"A"},{"text":"how","start":1522990,"end":1523230,"confidence":0.9995117,"speaker":"A"},{"text":"to","start":1523230,"end":1523430,"confidence":0.9902344,"speaker":"A"},{"text":"set","start":1523430,"end":1523630,"confidence":1,"speaker":"A"},{"text":"up.","start":1523630,"end":1523790,"confidence":0.984375,"speaker":"A"},{"text":"I'm","start":1523790,"end":1523990,"confidence":0.9970703,"speaker":"A"},{"text":"going","start":1523990,"end":1524070,"confidence":0.5854492,"speaker":"A"},{"text":"to","start":1524070,"end":1524150,"confidence":0.9951172,"speaker":"A"},{"text":"go","start":1524150,"end":1524269,"confidence":0.9975586,"speaker":"A"},{"text":"back","start":1524269,"end":1524429,"confidence":0.9995117,"speaker":"A"},{"text":"a","start":1524429,"end":1524550,"confidence":0.99902344,"speaker":"A"},{"text":"little","start":1524550,"end":1524630,"confidence":1,"speaker":"A"},{"text":"bit","start":1524630,"end":1524750,"confidence":0.99853516,"speaker":"A"},{"text":"Evan,","start":1524750,"end":1525190,"confidence":0.86279297,"speaker":"A"},{"text":"but","start":1525510,"end":1525790,"confidence":0.98535156,"speaker":"A"},{"text":"not","start":1525790,"end":1525950,"confidence":0.99316406,"speaker":"A"},{"text":"too","start":1525950,"end":1526110,"confidence":0.9980469,"speaker":"A"},{"text":"far","start":1526110,"end":1526310,"confidence":1,"speaker":"A"},{"text":"back.","start":1526310,"end":1526630,"confidence":0.99853516,"speaker":"A"}]},{"text":"Yeah, no worries. That's okay. But we talked about setting up API token and how to do that. So you go in here, you just click plus, you select your sign in callback and you put in a name and it'll give you an API token once you click save. Basically.","start":1527110,"end":1546310,"confidence":0.9895833,"words":[{"text":"Yeah,","start":1527110,"end":1527430,"confidence":0.9895833,"speaker":"B"},{"text":"no","start":1527430,"end":1527550,"confidence":0.9824219,"speaker":"B"},{"text":"worries.","start":1527550,"end":1527910,"confidence":0.998291,"speaker":"B"},{"text":"That's","start":1527990,"end":1528310,"confidence":0.99625653,"speaker":"A"},{"text":"okay.","start":1528310,"end":1528710,"confidence":0.9635417,"speaker":"A"},{"text":"But","start":1530470,"end":1530750,"confidence":0.9370117,"speaker":"A"},{"text":"we","start":1530750,"end":1530910,"confidence":0.9995117,"speaker":"A"},{"text":"talked","start":1530910,"end":1531110,"confidence":0.97265625,"speaker":"A"},{"text":"about","start":1531110,"end":1531270,"confidence":0.9980469,"speaker":"A"},{"text":"setting","start":1531270,"end":1531510,"confidence":0.9995117,"speaker":"A"},{"text":"up","start":1531510,"end":1531750,"confidence":0.99902344,"speaker":"A"},{"text":"API","start":1531830,"end":1532390,"confidence":0.9980469,"speaker":"A"},{"text":"token","start":1532390,"end":1532950,"confidence":1,"speaker":"A"},{"text":"and","start":1533270,"end":1533590,"confidence":0.9946289,"speaker":"A"},{"text":"how","start":1533590,"end":1533790,"confidence":1,"speaker":"A"},{"text":"to","start":1533790,"end":1533910,"confidence":0.9995117,"speaker":"A"},{"text":"do","start":1533910,"end":1534030,"confidence":1,"speaker":"A"},{"text":"that.","start":1534030,"end":1534310,"confidence":0.9995117,"speaker":"A"},{"text":"So","start":1535910,"end":1536150,"confidence":0.9707031,"speaker":"A"},{"text":"you","start":1536950,"end":1537350,"confidence":0.9169922,"speaker":"A"},{"text":"go","start":1537430,"end":1537710,"confidence":0.99072266,"speaker":"A"},{"text":"in","start":1537710,"end":1537870,"confidence":0.9941406,"speaker":"A"},{"text":"here,","start":1537870,"end":1538150,"confidence":0.9995117,"speaker":"A"},{"text":"you","start":1538150,"end":1538430,"confidence":0.9819336,"speaker":"A"},{"text":"just","start":1538430,"end":1538550,"confidence":0.9970703,"speaker":"A"},{"text":"click","start":1538550,"end":1538790,"confidence":0.9995117,"speaker":"A"},{"text":"plus,","start":1538790,"end":1539110,"confidence":0.9655762,"speaker":"A"},{"text":"you","start":1539110,"end":1539350,"confidence":0.9897461,"speaker":"A"},{"text":"select","start":1539350,"end":1539630,"confidence":0.9995117,"speaker":"A"},{"text":"your","start":1539630,"end":1539790,"confidence":0.9975586,"speaker":"A"},{"text":"sign","start":1539790,"end":1539990,"confidence":0.99658203,"speaker":"A"},{"text":"in","start":1539990,"end":1540190,"confidence":0.9428711,"speaker":"A"},{"text":"callback","start":1540190,"end":1540710,"confidence":0.9742839,"speaker":"A"},{"text":"and","start":1540710,"end":1540950,"confidence":0.99365234,"speaker":"A"},{"text":"you","start":1540950,"end":1541150,"confidence":0.98828125,"speaker":"A"},{"text":"put","start":1541150,"end":1541310,"confidence":1,"speaker":"A"},{"text":"in","start":1541310,"end":1541470,"confidence":0.9379883,"speaker":"A"},{"text":"a","start":1541470,"end":1541670,"confidence":0.9404297,"speaker":"A"},{"text":"name","start":1541670,"end":1541990,"confidence":0.99902344,"speaker":"A"},{"text":"and","start":1542630,"end":1542910,"confidence":0.90283203,"speaker":"A"},{"text":"it'll","start":1542910,"end":1543150,"confidence":0.84277344,"speaker":"A"},{"text":"give","start":1543150,"end":1543310,"confidence":1,"speaker":"A"},{"text":"you","start":1543310,"end":1543590,"confidence":0.9995117,"speaker":"A"},{"text":"an","start":1543750,"end":1544030,"confidence":0.9770508,"speaker":"A"},{"text":"API","start":1544030,"end":1544470,"confidence":0.8105469,"speaker":"A"},{"text":"token","start":1544470,"end":1544950,"confidence":0.9941406,"speaker":"A"},{"text":"once","start":1544950,"end":1545150,"confidence":0.99902344,"speaker":"A"},{"text":"you","start":1545150,"end":1545310,"confidence":0.9995117,"speaker":"A"},{"text":"click","start":1545310,"end":1545550,"confidence":0.99975586,"speaker":"A"},{"text":"save.","start":1545550,"end":1545830,"confidence":0.9980469,"speaker":"A"},{"text":"Basically.","start":1545830,"end":1546310,"confidence":0.9953613,"speaker":"A"}]},{"text":"Come on.","start":1550549,"end":1551190,"confidence":0.9658203,"words":[{"text":"Come","start":1550549,"end":1550870,"confidence":0.9658203,"speaker":"A"},{"text":"on.","start":1550870,"end":1551190,"confidence":0.99853516,"speaker":"A"}]},{"text":"The reason you want an API token is this allows you to then have users Sign in to CloudKit either using, using the the web service like Curl or you could also do it through a website using CloudKit js. So web authentication token we talked about how you can either do the post message or you can do the URL redirect. Basically you have the JavaScript on your website and there has a button, click the button, you get this nice little window here sign in and then when you sign in if you had selected post message, you'll get the web authentication token and the data of the event in JavaScript or you will get the web authentication token as a URL in the callback URL here. Does that make sense?","start":1554470,"end":1607820,"confidence":0.9975586,"words":[{"text":"The","start":1554470,"end":1554710,"confidence":0.9975586,"speaker":"A"},{"text":"reason","start":1554710,"end":1554910,"confidence":1,"speaker":"A"},{"text":"you","start":1554910,"end":1555150,"confidence":0.84814453,"speaker":"A"},{"text":"want","start":1555150,"end":1555310,"confidence":0.99902344,"speaker":"A"},{"text":"an","start":1555310,"end":1555470,"confidence":0.99658203,"speaker":"A"},{"text":"API","start":1555470,"end":1555830,"confidence":0.79589844,"speaker":"A"},{"text":"token","start":1555830,"end":1556190,"confidence":0.9998372,"speaker":"A"},{"text":"is","start":1556190,"end":1556390,"confidence":0.9941406,"speaker":"A"},{"text":"this","start":1556390,"end":1556590,"confidence":0.99902344,"speaker":"A"},{"text":"allows","start":1556590,"end":1556990,"confidence":0.99975586,"speaker":"A"},{"text":"you","start":1556990,"end":1557190,"confidence":0.9995117,"speaker":"A"},{"text":"to","start":1557190,"end":1557390,"confidence":0.9946289,"speaker":"A"},{"text":"then","start":1557390,"end":1557670,"confidence":0.95654297,"speaker":"A"},{"text":"have","start":1558550,"end":1558830,"confidence":0.9995117,"speaker":"A"},{"text":"users","start":1558830,"end":1559350,"confidence":0.99886066,"speaker":"A"},{"text":"Sign","start":1559350,"end":1559670,"confidence":1,"speaker":"A"},{"text":"in","start":1559670,"end":1559990,"confidence":0.9448242,"speaker":"A"},{"text":"to","start":1559990,"end":1560390,"confidence":0.9980469,"speaker":"A"},{"text":"CloudKit","start":1560390,"end":1561190,"confidence":0.97046,"speaker":"A"},{"text":"either","start":1562820,"end":1563060,"confidence":0.99902344,"speaker":"A"},{"text":"using,","start":1563060,"end":1563380,"confidence":0.9873047,"speaker":"A"},{"text":"using","start":1565140,"end":1565500,"confidence":1,"speaker":"A"},{"text":"the","start":1565500,"end":1565860,"confidence":0.9794922,"speaker":"A"},{"text":"the","start":1566420,"end":1566700,"confidence":0.99853516,"speaker":"A"},{"text":"web","start":1566700,"end":1567060,"confidence":0.99975586,"speaker":"A"},{"text":"service","start":1567140,"end":1567540,"confidence":0.9995117,"speaker":"A"},{"text":"like","start":1567620,"end":1567940,"confidence":0.9995117,"speaker":"A"},{"text":"Curl","start":1567940,"end":1568580,"confidence":0.8334961,"speaker":"A"},{"text":"or","start":1568900,"end":1569300,"confidence":1,"speaker":"A"},{"text":"you","start":1569300,"end":1569580,"confidence":0.9995117,"speaker":"A"},{"text":"could","start":1569580,"end":1569820,"confidence":0.99609375,"speaker":"A"},{"text":"also","start":1569820,"end":1570140,"confidence":1,"speaker":"A"},{"text":"do","start":1570140,"end":1570380,"confidence":1,"speaker":"A"},{"text":"it","start":1570380,"end":1570540,"confidence":1,"speaker":"A"},{"text":"through","start":1570540,"end":1570700,"confidence":1,"speaker":"A"},{"text":"a","start":1570700,"end":1570860,"confidence":1,"speaker":"A"},{"text":"website","start":1570860,"end":1571100,"confidence":0.9995117,"speaker":"A"},{"text":"using","start":1571100,"end":1571380,"confidence":0.9995117,"speaker":"A"},{"text":"CloudKit","start":1571380,"end":1571980,"confidence":0.998291,"speaker":"A"},{"text":"js.","start":1571980,"end":1572500,"confidence":0.83740234,"speaker":"A"},{"text":"So","start":1573780,"end":1574180,"confidence":0.99560547,"speaker":"A"},{"text":"web","start":1574420,"end":1574820,"confidence":0.97021484,"speaker":"A"},{"text":"authentication","start":1574820,"end":1575500,"confidence":0.9995117,"speaker":"A"},{"text":"token","start":1575500,"end":1576100,"confidence":0.9991862,"speaker":"A"},{"text":"we","start":1576100,"end":1576420,"confidence":0.9995117,"speaker":"A"},{"text":"talked","start":1576420,"end":1576700,"confidence":0.99975586,"speaker":"A"},{"text":"about","start":1576700,"end":1576900,"confidence":0.99902344,"speaker":"A"},{"text":"how","start":1576900,"end":1577219,"confidence":0.9995117,"speaker":"A"},{"text":"you","start":1577219,"end":1577460,"confidence":1,"speaker":"A"},{"text":"can","start":1577460,"end":1577539,"confidence":1,"speaker":"A"},{"text":"either","start":1577539,"end":1577740,"confidence":1,"speaker":"A"},{"text":"do","start":1577740,"end":1577900,"confidence":1,"speaker":"A"},{"text":"the","start":1577900,"end":1578060,"confidence":1,"speaker":"A"},{"text":"post","start":1578060,"end":1578300,"confidence":1,"speaker":"A"},{"text":"message","start":1578300,"end":1578780,"confidence":0.9995117,"speaker":"A"},{"text":"or","start":1578780,"end":1578980,"confidence":0.8930664,"speaker":"A"},{"text":"you","start":1578980,"end":1579140,"confidence":0.99902344,"speaker":"A"},{"text":"can","start":1579140,"end":1579260,"confidence":0.99853516,"speaker":"A"},{"text":"do","start":1579260,"end":1579380,"confidence":1,"speaker":"A"},{"text":"the","start":1579380,"end":1579500,"confidence":0.99853516,"speaker":"A"},{"text":"URL","start":1579500,"end":1579860,"confidence":0.77905273,"speaker":"A"},{"text":"redirect.","start":1579860,"end":1580420,"confidence":0.99975586,"speaker":"A"},{"text":"Basically","start":1581140,"end":1581700,"confidence":0.99975586,"speaker":"A"},{"text":"you","start":1581700,"end":1582100,"confidence":1,"speaker":"A"},{"text":"have","start":1582100,"end":1582380,"confidence":1,"speaker":"A"},{"text":"the","start":1582380,"end":1582540,"confidence":0.99121094,"speaker":"A"},{"text":"JavaScript","start":1582540,"end":1583020,"confidence":0.9979655,"speaker":"A"},{"text":"on","start":1583020,"end":1583180,"confidence":1,"speaker":"A"},{"text":"your","start":1583180,"end":1583380,"confidence":1,"speaker":"A"},{"text":"website","start":1583380,"end":1583700,"confidence":0.9951172,"speaker":"A"},{"text":"and","start":1584820,"end":1585180,"confidence":0.9980469,"speaker":"A"},{"text":"there","start":1585180,"end":1585420,"confidence":0.58447266,"speaker":"A"},{"text":"has","start":1585420,"end":1585580,"confidence":0.8017578,"speaker":"A"},{"text":"a","start":1585580,"end":1585700,"confidence":1,"speaker":"A"},{"text":"button,","start":1585700,"end":1585980,"confidence":0.998291,"speaker":"A"},{"text":"click","start":1585980,"end":1586260,"confidence":0.99975586,"speaker":"A"},{"text":"the","start":1586260,"end":1586380,"confidence":0.9995117,"speaker":"A"},{"text":"button,","start":1586380,"end":1586620,"confidence":0.99975586,"speaker":"A"},{"text":"you","start":1586620,"end":1586740,"confidence":0.99853516,"speaker":"A"},{"text":"get","start":1586740,"end":1586860,"confidence":0.99560547,"speaker":"A"},{"text":"this","start":1586860,"end":1587020,"confidence":0.9995117,"speaker":"A"},{"text":"nice","start":1587020,"end":1587260,"confidence":0.99975586,"speaker":"A"},{"text":"little","start":1587260,"end":1587460,"confidence":0.9995117,"speaker":"A"},{"text":"window","start":1587460,"end":1587820,"confidence":0.99975586,"speaker":"A"},{"text":"here","start":1587820,"end":1588100,"confidence":0.9951172,"speaker":"A"},{"text":"sign","start":1588780,"end":1588940,"confidence":0.95947266,"speaker":"A"},{"text":"in","start":1588940,"end":1589260,"confidence":0.99072266,"speaker":"A"},{"text":"and","start":1590860,"end":1591140,"confidence":0.9550781,"speaker":"A"},{"text":"then","start":1591140,"end":1591420,"confidence":0.9970703,"speaker":"A"},{"text":"when","start":1591820,"end":1592100,"confidence":1,"speaker":"A"},{"text":"you","start":1592100,"end":1592300,"confidence":0.9995117,"speaker":"A"},{"text":"sign","start":1592300,"end":1592540,"confidence":0.9995117,"speaker":"A"},{"text":"in","start":1592540,"end":1592820,"confidence":0.98583984,"speaker":"A"},{"text":"if","start":1592820,"end":1593060,"confidence":0.9980469,"speaker":"A"},{"text":"you","start":1593060,"end":1593340,"confidence":0.9995117,"speaker":"A"},{"text":"had","start":1593340,"end":1593660,"confidence":0.9121094,"speaker":"A"},{"text":"selected","start":1593660,"end":1594060,"confidence":0.9992676,"speaker":"A"},{"text":"post","start":1594060,"end":1594380,"confidence":0.9975586,"speaker":"A"},{"text":"message,","start":1594380,"end":1595020,"confidence":0.984375,"speaker":"A"},{"text":"you'll","start":1595340,"end":1595700,"confidence":0.9923503,"speaker":"A"},{"text":"get","start":1595700,"end":1595860,"confidence":0.9995117,"speaker":"A"},{"text":"the","start":1595860,"end":1596020,"confidence":0.9995117,"speaker":"A"},{"text":"web","start":1596020,"end":1596260,"confidence":0.9992676,"speaker":"A"},{"text":"authentication","start":1596260,"end":1597020,"confidence":0.96813965,"speaker":"A"},{"text":"token","start":1597020,"end":1597540,"confidence":0.9998372,"speaker":"A"},{"text":"and","start":1597540,"end":1597820,"confidence":0.5283203,"speaker":"A"},{"text":"the","start":1597820,"end":1598020,"confidence":0.9995117,"speaker":"A"},{"text":"data","start":1598020,"end":1598260,"confidence":0.9995117,"speaker":"A"},{"text":"of","start":1598260,"end":1598500,"confidence":0.9995117,"speaker":"A"},{"text":"the","start":1598500,"end":1598660,"confidence":0.9995117,"speaker":"A"},{"text":"event","start":1598660,"end":1598940,"confidence":0.99853516,"speaker":"A"},{"text":"in","start":1598940,"end":1599260,"confidence":0.9291992,"speaker":"A"},{"text":"JavaScript","start":1599260,"end":1600060,"confidence":0.99348956,"speaker":"A"},{"text":"or","start":1600540,"end":1600900,"confidence":0.99902344,"speaker":"A"},{"text":"you","start":1600900,"end":1601140,"confidence":0.9995117,"speaker":"A"},{"text":"will","start":1601140,"end":1601300,"confidence":0.87109375,"speaker":"A"},{"text":"get","start":1601300,"end":1601460,"confidence":1,"speaker":"A"},{"text":"the","start":1601460,"end":1601580,"confidence":0.9995117,"speaker":"A"},{"text":"web","start":1601580,"end":1601780,"confidence":0.9980469,"speaker":"A"},{"text":"authentication","start":1601780,"end":1602460,"confidence":0.8979492,"speaker":"A"},{"text":"token","start":1602460,"end":1602860,"confidence":0.9996745,"speaker":"A"},{"text":"as","start":1602860,"end":1603060,"confidence":0.9995117,"speaker":"A"},{"text":"a","start":1603060,"end":1603220,"confidence":0.98779297,"speaker":"A"},{"text":"URL","start":1603220,"end":1603820,"confidence":0.86157227,"speaker":"A"},{"text":"in","start":1604300,"end":1604579,"confidence":0.99853516,"speaker":"A"},{"text":"the","start":1604579,"end":1604739,"confidence":1,"speaker":"A"},{"text":"callback","start":1604739,"end":1605260,"confidence":0.9983724,"speaker":"A"},{"text":"URL","start":1605260,"end":1605780,"confidence":0.8745117,"speaker":"A"},{"text":"here.","start":1605780,"end":1606140,"confidence":0.9975586,"speaker":"A"},{"text":"Does","start":1606780,"end":1607060,"confidence":0.9995117,"speaker":"A"},{"text":"that","start":1607060,"end":1607220,"confidence":0.9995117,"speaker":"A"},{"text":"make","start":1607220,"end":1607420,"confidence":0.9926758,"speaker":"A"},{"text":"sense?","start":1607420,"end":1607820,"confidence":0.9995117,"speaker":"A"}]},{"text":"Yep. Yeah. In some cases if you scour the Internet so Stack overflow will tell you and this has happened to me sometimes it will not be CK web authentication token, sometimes it'll be CK session because that's what Apple likes to do.","start":1610860,"end":1626600,"confidence":0.7561035,"words":[{"text":"Yep.","start":1610860,"end":1611420,"confidence":0.7561035,"speaker":"B"},{"text":"Yeah.","start":1612220,"end":1612860,"confidence":0.94124347,"speaker":"A"},{"text":"In","start":1613420,"end":1613740,"confidence":0.9975586,"speaker":"A"},{"text":"some","start":1613740,"end":1613940,"confidence":1,"speaker":"A"},{"text":"cases","start":1613940,"end":1614220,"confidence":0.9995117,"speaker":"A"},{"text":"if","start":1614380,"end":1614660,"confidence":0.99853516,"speaker":"A"},{"text":"you","start":1614660,"end":1614940,"confidence":1,"speaker":"A"},{"text":"scour","start":1615180,"end":1615620,"confidence":0.9921875,"speaker":"A"},{"text":"the","start":1615620,"end":1615860,"confidence":0.9995117,"speaker":"A"},{"text":"Internet","start":1615860,"end":1616295,"confidence":0.99780273,"speaker":"A"},{"text":"so","start":1616295,"end":1616450,"confidence":0.37280273,"speaker":"A"},{"text":"Stack","start":1616520,"end":1616720,"confidence":0.94799805,"speaker":"A"},{"text":"overflow","start":1616720,"end":1617120,"confidence":0.9749756,"speaker":"A"},{"text":"will","start":1617120,"end":1617280,"confidence":0.9916992,"speaker":"A"},{"text":"tell","start":1617280,"end":1617440,"confidence":1,"speaker":"A"},{"text":"you","start":1617440,"end":1617600,"confidence":0.9995117,"speaker":"A"},{"text":"and","start":1617600,"end":1617800,"confidence":0.99658203,"speaker":"A"},{"text":"this","start":1617800,"end":1618000,"confidence":0.99902344,"speaker":"A"},{"text":"has","start":1618000,"end":1618200,"confidence":0.9765625,"speaker":"A"},{"text":"happened","start":1618200,"end":1618520,"confidence":0.99975586,"speaker":"A"},{"text":"to","start":1618520,"end":1618640,"confidence":0.9995117,"speaker":"A"},{"text":"me","start":1618640,"end":1618920,"confidence":0.9995117,"speaker":"A"},{"text":"sometimes","start":1619240,"end":1619720,"confidence":0.9998372,"speaker":"A"},{"text":"it","start":1619720,"end":1619800,"confidence":0.99902344,"speaker":"A"},{"text":"will","start":1619800,"end":1619920,"confidence":0.99853516,"speaker":"A"},{"text":"not","start":1619920,"end":1620080,"confidence":0.9995117,"speaker":"A"},{"text":"be","start":1620080,"end":1620360,"confidence":0.99902344,"speaker":"A"},{"text":"CK","start":1620360,"end":1620920,"confidence":0.89404297,"speaker":"A"},{"text":"web","start":1620920,"end":1621200,"confidence":0.9916992,"speaker":"A"},{"text":"authentication","start":1621200,"end":1621880,"confidence":0.9996338,"speaker":"A"},{"text":"token,","start":1621880,"end":1622360,"confidence":0.9995117,"speaker":"A"},{"text":"sometimes","start":1622360,"end":1622760,"confidence":0.9954427,"speaker":"A"},{"text":"it'll","start":1622760,"end":1623000,"confidence":0.8121745,"speaker":"A"},{"text":"be","start":1623000,"end":1623080,"confidence":0.9995117,"speaker":"A"},{"text":"CK","start":1623080,"end":1623480,"confidence":0.8876953,"speaker":"A"},{"text":"session","start":1623480,"end":1624040,"confidence":0.99902344,"speaker":"A"},{"text":"because","start":1624360,"end":1624760,"confidence":0.99853516,"speaker":"A"},{"text":"that's","start":1625240,"end":1625600,"confidence":0.9996745,"speaker":"A"},{"text":"what","start":1625600,"end":1625760,"confidence":0.99560547,"speaker":"A"},{"text":"Apple","start":1625760,"end":1626040,"confidence":0.99560547,"speaker":"A"},{"text":"likes","start":1626040,"end":1626280,"confidence":0.98999023,"speaker":"A"},{"text":"to","start":1626280,"end":1626360,"confidence":0.9995117,"speaker":"A"},{"text":"do.","start":1626360,"end":1626600,"confidence":0.9995117,"speaker":"A"}]},{"text":"But it's the same thing. So you basically want to look for either property or query parameter name and you should be good to go and then you'll have that user as well authentication token you could do. What I, what I've been doing is, is I've been take like making a call to a like local server for instance and then essentially then I could do whatever I want with that web authentication token. As long as you have the web authentication token and the API token you can do anything on a private database that the user has rights to. So you can go, you can go to town with that all this stuff gets Swift in a cookie too.","start":1629080,"end":1671420,"confidence":0.99316406,"words":[{"text":"But","start":1629080,"end":1629360,"confidence":0.99316406,"speaker":"A"},{"text":"it's","start":1629360,"end":1629560,"confidence":0.9995117,"speaker":"A"},{"text":"the","start":1629560,"end":1629680,"confidence":1,"speaker":"A"},{"text":"same","start":1629680,"end":1629840,"confidence":1,"speaker":"A"},{"text":"thing.","start":1629840,"end":1630120,"confidence":0.9995117,"speaker":"A"},{"text":"So","start":1630200,"end":1630480,"confidence":0.99853516,"speaker":"A"},{"text":"you","start":1630480,"end":1630640,"confidence":0.9980469,"speaker":"A"},{"text":"basically","start":1630640,"end":1630920,"confidence":0.99975586,"speaker":"A"},{"text":"want","start":1630920,"end":1631120,"confidence":0.8725586,"speaker":"A"},{"text":"to","start":1631120,"end":1631240,"confidence":1,"speaker":"A"},{"text":"look","start":1631240,"end":1631320,"confidence":1,"speaker":"A"},{"text":"for","start":1631320,"end":1631440,"confidence":1,"speaker":"A"},{"text":"either","start":1631440,"end":1631720,"confidence":0.99975586,"speaker":"A"},{"text":"property","start":1631720,"end":1632200,"confidence":0.99902344,"speaker":"A"},{"text":"or","start":1632200,"end":1632520,"confidence":0.9995117,"speaker":"A"},{"text":"query","start":1632680,"end":1633160,"confidence":0.97436523,"speaker":"A"},{"text":"parameter","start":1633240,"end":1633840,"confidence":0.9998372,"speaker":"A"},{"text":"name","start":1633840,"end":1634160,"confidence":0.99853516,"speaker":"A"},{"text":"and","start":1634160,"end":1634400,"confidence":0.99853516,"speaker":"A"},{"text":"you","start":1634400,"end":1634560,"confidence":0.9980469,"speaker":"A"},{"text":"should","start":1634560,"end":1634720,"confidence":1,"speaker":"A"},{"text":"be","start":1634720,"end":1634880,"confidence":1,"speaker":"A"},{"text":"good","start":1634880,"end":1635040,"confidence":0.9995117,"speaker":"A"},{"text":"to","start":1635040,"end":1635200,"confidence":0.9980469,"speaker":"A"},{"text":"go","start":1635200,"end":1635480,"confidence":0.9975586,"speaker":"A"},{"text":"and","start":1636360,"end":1636640,"confidence":0.99560547,"speaker":"A"},{"text":"then","start":1636640,"end":1636760,"confidence":1,"speaker":"A"},{"text":"you'll","start":1636760,"end":1636960,"confidence":0.9902344,"speaker":"A"},{"text":"have","start":1636960,"end":1637080,"confidence":0.9995117,"speaker":"A"},{"text":"that","start":1637080,"end":1637160,"confidence":0.99902344,"speaker":"A"},{"text":"user","start":1637160,"end":1637400,"confidence":0.99902344,"speaker":"A"},{"text":"as","start":1637400,"end":1637520,"confidence":0.4970703,"speaker":"A"},{"text":"well","start":1637520,"end":1637800,"confidence":0.99316406,"speaker":"A"},{"text":"authentication","start":1637800,"end":1638520,"confidence":0.99902344,"speaker":"A"},{"text":"token","start":1638520,"end":1639080,"confidence":0.9995117,"speaker":"A"},{"text":"you","start":1639960,"end":1640240,"confidence":0.98876953,"speaker":"A"},{"text":"could","start":1640240,"end":1640400,"confidence":0.9658203,"speaker":"A"},{"text":"do.","start":1640400,"end":1640680,"confidence":0.9926758,"speaker":"A"},{"text":"What","start":1640920,"end":1641240,"confidence":0.9736328,"speaker":"A"},{"text":"I,","start":1641240,"end":1641560,"confidence":0.9926758,"speaker":"A"},{"text":"what","start":1641720,"end":1642000,"confidence":0.9086914,"speaker":"A"},{"text":"I've","start":1642000,"end":1642200,"confidence":0.99527997,"speaker":"A"},{"text":"been","start":1642200,"end":1642360,"confidence":0.9995117,"speaker":"A"},{"text":"doing","start":1642360,"end":1642680,"confidence":0.9995117,"speaker":"A"},{"text":"is,","start":1643490,"end":1643730,"confidence":0.9863281,"speaker":"A"},{"text":"is","start":1645170,"end":1645490,"confidence":0.94628906,"speaker":"A"},{"text":"I've","start":1645490,"end":1645850,"confidence":0.9996745,"speaker":"A"},{"text":"been","start":1645850,"end":1646130,"confidence":0.99853516,"speaker":"A"},{"text":"take","start":1647330,"end":1647730,"confidence":0.9165039,"speaker":"A"},{"text":"like","start":1647730,"end":1648050,"confidence":0.99902344,"speaker":"A"},{"text":"making","start":1648050,"end":1648290,"confidence":0.99902344,"speaker":"A"},{"text":"a","start":1648290,"end":1648490,"confidence":0.9995117,"speaker":"A"},{"text":"call","start":1648490,"end":1648690,"confidence":1,"speaker":"A"},{"text":"to","start":1648690,"end":1648930,"confidence":0.9995117,"speaker":"A"},{"text":"a","start":1648930,"end":1649130,"confidence":0.7597656,"speaker":"A"},{"text":"like","start":1649130,"end":1649370,"confidence":0.98779297,"speaker":"A"},{"text":"local","start":1649370,"end":1649690,"confidence":0.9995117,"speaker":"A"},{"text":"server","start":1649690,"end":1650170,"confidence":0.99975586,"speaker":"A"},{"text":"for","start":1650170,"end":1650330,"confidence":0.9995117,"speaker":"A"},{"text":"instance","start":1650330,"end":1650770,"confidence":0.99975586,"speaker":"A"},{"text":"and","start":1651330,"end":1651650,"confidence":0.99853516,"speaker":"A"},{"text":"then","start":1651650,"end":1651970,"confidence":0.99902344,"speaker":"A"},{"text":"essentially","start":1651970,"end":1652690,"confidence":0.9987793,"speaker":"A"},{"text":"then","start":1653410,"end":1653690,"confidence":0.8886719,"speaker":"A"},{"text":"I","start":1653690,"end":1653810,"confidence":1,"speaker":"A"},{"text":"could","start":1653810,"end":1653930,"confidence":0.6508789,"speaker":"A"},{"text":"do","start":1653930,"end":1654090,"confidence":0.9995117,"speaker":"A"},{"text":"whatever","start":1654090,"end":1654330,"confidence":1,"speaker":"A"},{"text":"I","start":1654330,"end":1654490,"confidence":0.9995117,"speaker":"A"},{"text":"want","start":1654490,"end":1654690,"confidence":0.9995117,"speaker":"A"},{"text":"with","start":1654690,"end":1654890,"confidence":0.99853516,"speaker":"A"},{"text":"that","start":1654890,"end":1655050,"confidence":0.9995117,"speaker":"A"},{"text":"web","start":1655050,"end":1655290,"confidence":0.9897461,"speaker":"A"},{"text":"authentication","start":1655290,"end":1655970,"confidence":0.9991455,"speaker":"A"},{"text":"token.","start":1655970,"end":1656330,"confidence":0.9996745,"speaker":"A"},{"text":"As","start":1656330,"end":1656490,"confidence":0.9995117,"speaker":"A"},{"text":"long","start":1656490,"end":1656610,"confidence":1,"speaker":"A"},{"text":"as","start":1656610,"end":1656690,"confidence":0.99853516,"speaker":"A"},{"text":"you","start":1656690,"end":1656770,"confidence":1,"speaker":"A"},{"text":"have","start":1656770,"end":1656890,"confidence":0.9995117,"speaker":"A"},{"text":"the","start":1656890,"end":1657010,"confidence":0.9995117,"speaker":"A"},{"text":"web","start":1657010,"end":1657210,"confidence":0.998291,"speaker":"A"},{"text":"authentication","start":1657210,"end":1657730,"confidence":0.99975586,"speaker":"A"},{"text":"token","start":1657730,"end":1658090,"confidence":0.99902344,"speaker":"A"},{"text":"and","start":1658090,"end":1658210,"confidence":0.9355469,"speaker":"A"},{"text":"the","start":1658210,"end":1658330,"confidence":0.99853516,"speaker":"A"},{"text":"API","start":1658330,"end":1658770,"confidence":0.9987793,"speaker":"A"},{"text":"token","start":1658770,"end":1659329,"confidence":0.9996745,"speaker":"A"},{"text":"you","start":1659570,"end":1659850,"confidence":0.9995117,"speaker":"A"},{"text":"can","start":1659850,"end":1660010,"confidence":0.9995117,"speaker":"A"},{"text":"do","start":1660010,"end":1660170,"confidence":1,"speaker":"A"},{"text":"anything","start":1660170,"end":1660570,"confidence":0.99975586,"speaker":"A"},{"text":"on","start":1660570,"end":1660730,"confidence":0.9995117,"speaker":"A"},{"text":"a","start":1660730,"end":1660850,"confidence":0.99902344,"speaker":"A"},{"text":"private","start":1660850,"end":1661050,"confidence":1,"speaker":"A"},{"text":"database","start":1661050,"end":1661810,"confidence":0.99934894,"speaker":"A"},{"text":"that","start":1662530,"end":1662810,"confidence":0.9975586,"speaker":"A"},{"text":"the","start":1662810,"end":1662930,"confidence":0.9995117,"speaker":"A"},{"text":"user","start":1662930,"end":1663210,"confidence":1,"speaker":"A"},{"text":"has","start":1663210,"end":1663410,"confidence":0.99902344,"speaker":"A"},{"text":"rights","start":1663410,"end":1663690,"confidence":0.9975586,"speaker":"A"},{"text":"to.","start":1663690,"end":1664050,"confidence":0.9824219,"speaker":"A"},{"text":"So","start":1664450,"end":1664850,"confidence":0.9941406,"speaker":"A"},{"text":"you","start":1665890,"end":1666170,"confidence":0.98876953,"speaker":"A"},{"text":"can","start":1666170,"end":1666330,"confidence":0.95703125,"speaker":"A"},{"text":"go,","start":1666330,"end":1666570,"confidence":0.99853516,"speaker":"A"},{"text":"you","start":1666570,"end":1666810,"confidence":0.99560547,"speaker":"A"},{"text":"can","start":1666810,"end":1666970,"confidence":0.5966797,"speaker":"A"},{"text":"go","start":1666970,"end":1667130,"confidence":1,"speaker":"A"},{"text":"to","start":1667130,"end":1667250,"confidence":0.9980469,"speaker":"A"},{"text":"town","start":1667250,"end":1667410,"confidence":0.99902344,"speaker":"A"},{"text":"with","start":1667410,"end":1667610,"confidence":0.99609375,"speaker":"A"},{"text":"that","start":1667610,"end":1667890,"confidence":0.9848633,"speaker":"A"},{"text":"all","start":1669420,"end":1669540,"confidence":0.99365234,"speaker":"A"},{"text":"this","start":1669540,"end":1669700,"confidence":0.8154297,"speaker":"A"},{"text":"stuff","start":1669700,"end":1669900,"confidence":1,"speaker":"A"},{"text":"gets","start":1669900,"end":1670060,"confidence":0.99487305,"speaker":"A"},{"text":"Swift","start":1670060,"end":1670260,"confidence":0.99975586,"speaker":"A"},{"text":"in","start":1670260,"end":1670420,"confidence":0.99902344,"speaker":"A"},{"text":"a","start":1670420,"end":1670540,"confidence":0.9995117,"speaker":"A"},{"text":"cookie","start":1670540,"end":1671020,"confidence":1,"speaker":"A"},{"text":"too.","start":1671020,"end":1671420,"confidence":0.9838867,"speaker":"A"}]},{"text":"So that way it'll work. When you go back, if you have checked the box for allow, it's either a box or JavaScript method property that will say, hey, I want this to persist. It'll be Swift in a, in a cookie as well. So if you want to spelunk your cookies, you can see the web authentication token there. So that's actually the easier of the two.","start":1671580,"end":1693500,"confidence":0.99658203,"words":[{"text":"So","start":1671580,"end":1671820,"confidence":0.99658203,"speaker":"A"},{"text":"that","start":1671820,"end":1671940,"confidence":1,"speaker":"A"},{"text":"way","start":1671940,"end":1672180,"confidence":0.9995117,"speaker":"A"},{"text":"it'll","start":1672180,"end":1672540,"confidence":0.8470052,"speaker":"A"},{"text":"work.","start":1672540,"end":1672860,"confidence":1,"speaker":"A"},{"text":"When","start":1673740,"end":1674020,"confidence":1,"speaker":"A"},{"text":"you","start":1674020,"end":1674220,"confidence":0.9995117,"speaker":"A"},{"text":"go","start":1674220,"end":1674460,"confidence":1,"speaker":"A"},{"text":"back,","start":1674460,"end":1674700,"confidence":1,"speaker":"A"},{"text":"if","start":1674700,"end":1674940,"confidence":0.53125,"speaker":"A"},{"text":"you","start":1674940,"end":1675260,"confidence":0.9995117,"speaker":"A"},{"text":"have","start":1675500,"end":1675900,"confidence":0.9995117,"speaker":"A"},{"text":"checked","start":1675900,"end":1676420,"confidence":0.99560547,"speaker":"A"},{"text":"the","start":1676420,"end":1676580,"confidence":1,"speaker":"A"},{"text":"box","start":1676580,"end":1676900,"confidence":0.9995117,"speaker":"A"},{"text":"for","start":1676900,"end":1677180,"confidence":0.99902344,"speaker":"A"},{"text":"allow,","start":1677180,"end":1677500,"confidence":0.99560547,"speaker":"A"},{"text":"it's","start":1678780,"end":1679100,"confidence":0.9899089,"speaker":"A"},{"text":"either","start":1679100,"end":1679340,"confidence":0.99975586,"speaker":"A"},{"text":"a","start":1679340,"end":1679540,"confidence":0.9995117,"speaker":"A"},{"text":"box","start":1679540,"end":1679780,"confidence":0.99975586,"speaker":"A"},{"text":"or","start":1679780,"end":1679980,"confidence":0.99902344,"speaker":"A"},{"text":"JavaScript","start":1679980,"end":1680580,"confidence":0.99934894,"speaker":"A"},{"text":"method","start":1680580,"end":1680900,"confidence":0.99348956,"speaker":"A"},{"text":"property","start":1680900,"end":1681260,"confidence":0.9995117,"speaker":"A"},{"text":"that","start":1681260,"end":1681460,"confidence":0.9995117,"speaker":"A"},{"text":"will","start":1681460,"end":1681700,"confidence":0.9013672,"speaker":"A"},{"text":"say,","start":1681700,"end":1681940,"confidence":0.9975586,"speaker":"A"},{"text":"hey,","start":1681940,"end":1682180,"confidence":0.9992676,"speaker":"A"},{"text":"I","start":1682180,"end":1682300,"confidence":1,"speaker":"A"},{"text":"want","start":1682300,"end":1682420,"confidence":1,"speaker":"A"},{"text":"this","start":1682420,"end":1682580,"confidence":0.99902344,"speaker":"A"},{"text":"to","start":1682580,"end":1682740,"confidence":1,"speaker":"A"},{"text":"persist.","start":1682740,"end":1683260,"confidence":0.9992676,"speaker":"A"},{"text":"It'll","start":1683420,"end":1683780,"confidence":0.9715169,"speaker":"A"},{"text":"be","start":1683780,"end":1683900,"confidence":1,"speaker":"A"},{"text":"Swift","start":1683900,"end":1684100,"confidence":0.9980469,"speaker":"A"},{"text":"in","start":1684100,"end":1684260,"confidence":0.9121094,"speaker":"A"},{"text":"a,","start":1684260,"end":1684420,"confidence":0.7871094,"speaker":"A"},{"text":"in","start":1684420,"end":1684580,"confidence":0.71191406,"speaker":"A"},{"text":"a","start":1684580,"end":1684740,"confidence":0.9995117,"speaker":"A"},{"text":"cookie","start":1684740,"end":1685020,"confidence":0.99975586,"speaker":"A"},{"text":"as","start":1685020,"end":1685179,"confidence":1,"speaker":"A"},{"text":"well.","start":1685179,"end":1685460,"confidence":1,"speaker":"A"},{"text":"So","start":1685460,"end":1685700,"confidence":0.99658203,"speaker":"A"},{"text":"if","start":1685700,"end":1685820,"confidence":1,"speaker":"A"},{"text":"you","start":1685820,"end":1685940,"confidence":1,"speaker":"A"},{"text":"want","start":1685940,"end":1686060,"confidence":0.95751953,"speaker":"A"},{"text":"to","start":1686060,"end":1686220,"confidence":0.97314453,"speaker":"A"},{"text":"spelunk","start":1686220,"end":1686820,"confidence":0.9758301,"speaker":"A"},{"text":"your","start":1686820,"end":1686980,"confidence":0.99560547,"speaker":"A"},{"text":"cookies,","start":1686980,"end":1687260,"confidence":1,"speaker":"A"},{"text":"you","start":1687340,"end":1687580,"confidence":0.99853516,"speaker":"A"},{"text":"can","start":1687580,"end":1687820,"confidence":0.9995117,"speaker":"A"},{"text":"see","start":1687980,"end":1688300,"confidence":0.78027344,"speaker":"A"},{"text":"the","start":1688300,"end":1688500,"confidence":0.9995117,"speaker":"A"},{"text":"web","start":1688500,"end":1688740,"confidence":0.9992676,"speaker":"A"},{"text":"authentication","start":1688740,"end":1689340,"confidence":0.99938965,"speaker":"A"},{"text":"token","start":1689340,"end":1689740,"confidence":0.99902344,"speaker":"A"},{"text":"there.","start":1689740,"end":1690060,"confidence":0.99560547,"speaker":"A"},{"text":"So","start":1691500,"end":1691780,"confidence":0.9921875,"speaker":"A"},{"text":"that's","start":1691780,"end":1692100,"confidence":0.9995117,"speaker":"A"},{"text":"actually","start":1692100,"end":1692300,"confidence":0.9995117,"speaker":"A"},{"text":"the","start":1692300,"end":1692540,"confidence":0.99609375,"speaker":"A"},{"text":"easier","start":1692540,"end":1692900,"confidence":0.99975586,"speaker":"A"},{"text":"of","start":1692900,"end":1693020,"confidence":0.9995117,"speaker":"A"},{"text":"the","start":1693020,"end":1693180,"confidence":0.99902344,"speaker":"A"},{"text":"two.","start":1693180,"end":1693500,"confidence":0.9926758,"speaker":"A"}]},{"text":"So that gives you the private database for the public database is where you're going to need a server to server authentication. And so to do that it's really actually not as bad as I thought it was going to be. But you go to the new server to server key, put in a name you want, it'll actually give you the command you need to run and then you just paste in the public key in here. That gives you. That will give you everything you need.","start":1694380,"end":1720300,"confidence":0.99902344,"words":[{"text":"So","start":1694380,"end":1694660,"confidence":0.99902344,"speaker":"A"},{"text":"that","start":1694660,"end":1694820,"confidence":1,"speaker":"A"},{"text":"gives","start":1694820,"end":1695020,"confidence":1,"speaker":"A"},{"text":"you","start":1695020,"end":1695100,"confidence":1,"speaker":"A"},{"text":"the","start":1695100,"end":1695220,"confidence":0.9995117,"speaker":"A"},{"text":"private","start":1695220,"end":1695420,"confidence":1,"speaker":"A"},{"text":"database","start":1695420,"end":1695940,"confidence":0.9998372,"speaker":"A"},{"text":"for","start":1695940,"end":1696100,"confidence":0.9975586,"speaker":"A"},{"text":"the","start":1696100,"end":1696220,"confidence":0.9995117,"speaker":"A"},{"text":"public","start":1696220,"end":1696380,"confidence":1,"speaker":"A"},{"text":"database","start":1696380,"end":1696940,"confidence":0.99886066,"speaker":"A"},{"text":"is","start":1696940,"end":1697140,"confidence":0.98876953,"speaker":"A"},{"text":"where","start":1697140,"end":1697300,"confidence":0.99902344,"speaker":"A"},{"text":"you're","start":1697300,"end":1697500,"confidence":0.9975586,"speaker":"A"},{"text":"going","start":1697500,"end":1697580,"confidence":0.9355469,"speaker":"A"},{"text":"to","start":1697580,"end":1697660,"confidence":0.9980469,"speaker":"A"},{"text":"need","start":1697660,"end":1697820,"confidence":0.99853516,"speaker":"A"},{"text":"a","start":1697820,"end":1697990,"confidence":0.55908203,"speaker":"A"},{"text":"server","start":1698220,"end":1698460,"confidence":0.9975586,"speaker":"A"},{"text":"to","start":1698460,"end":1698620,"confidence":0.9536133,"speaker":"A"},{"text":"server","start":1698620,"end":1699020,"confidence":0.99902344,"speaker":"A"},{"text":"authentication.","start":1699020,"end":1699820,"confidence":0.99938965,"speaker":"A"},{"text":"And","start":1701340,"end":1701700,"confidence":0.98876953,"speaker":"A"},{"text":"so","start":1701700,"end":1701940,"confidence":0.9995117,"speaker":"A"},{"text":"to","start":1701940,"end":1702100,"confidence":0.9995117,"speaker":"A"},{"text":"do","start":1702100,"end":1702300,"confidence":0.9995117,"speaker":"A"},{"text":"that","start":1702300,"end":1702620,"confidence":0.9970703,"speaker":"A"},{"text":"it's","start":1703180,"end":1703540,"confidence":0.9996745,"speaker":"A"},{"text":"really","start":1703540,"end":1703820,"confidence":0.99853516,"speaker":"A"},{"text":"actually","start":1703820,"end":1704180,"confidence":0.99853516,"speaker":"A"},{"text":"not","start":1704180,"end":1704420,"confidence":1,"speaker":"A"},{"text":"as","start":1704420,"end":1704620,"confidence":0.99902344,"speaker":"A"},{"text":"bad","start":1704620,"end":1704820,"confidence":1,"speaker":"A"},{"text":"as","start":1704820,"end":1704980,"confidence":0.9995117,"speaker":"A"},{"text":"I","start":1704980,"end":1705140,"confidence":1,"speaker":"A"},{"text":"thought","start":1705140,"end":1705260,"confidence":1,"speaker":"A"},{"text":"it","start":1705260,"end":1705340,"confidence":0.9975586,"speaker":"A"},{"text":"was","start":1705340,"end":1705460,"confidence":0.9995117,"speaker":"A"},{"text":"going","start":1705460,"end":1705580,"confidence":0.8984375,"speaker":"A"},{"text":"to","start":1705580,"end":1705660,"confidence":1,"speaker":"A"},{"text":"be.","start":1705660,"end":1705900,"confidence":1,"speaker":"A"},{"text":"But","start":1705900,"end":1706300,"confidence":0.9975586,"speaker":"A"},{"text":"you","start":1706620,"end":1706940,"confidence":0.9995117,"speaker":"A"},{"text":"go","start":1706940,"end":1707220,"confidence":0.99902344,"speaker":"A"},{"text":"to","start":1707220,"end":1707500,"confidence":1,"speaker":"A"},{"text":"the","start":1707500,"end":1707700,"confidence":0.9995117,"speaker":"A"},{"text":"new","start":1707700,"end":1707980,"confidence":0.9970703,"speaker":"A"},{"text":"server","start":1708220,"end":1708620,"confidence":0.99731445,"speaker":"A"},{"text":"to","start":1708620,"end":1708740,"confidence":0.8359375,"speaker":"A"},{"text":"server","start":1708740,"end":1709140,"confidence":0.99731445,"speaker":"A"},{"text":"key,","start":1709140,"end":1709420,"confidence":0.99121094,"speaker":"A"},{"text":"put","start":1709420,"end":1709700,"confidence":0.9951172,"speaker":"A"},{"text":"in","start":1709700,"end":1709900,"confidence":0.9526367,"speaker":"A"},{"text":"a","start":1709900,"end":1710100,"confidence":0.9555664,"speaker":"A"},{"text":"name","start":1710100,"end":1710300,"confidence":0.9941406,"speaker":"A"},{"text":"you","start":1710300,"end":1710500,"confidence":0.99072266,"speaker":"A"},{"text":"want,","start":1710500,"end":1710780,"confidence":0.70458984,"speaker":"A"},{"text":"it'll","start":1711020,"end":1711460,"confidence":0.9889323,"speaker":"A"},{"text":"actually","start":1711460,"end":1711660,"confidence":0.99902344,"speaker":"A"},{"text":"give","start":1711660,"end":1711860,"confidence":1,"speaker":"A"},{"text":"you","start":1711860,"end":1712020,"confidence":0.9995117,"speaker":"A"},{"text":"the","start":1712020,"end":1712180,"confidence":0.9995117,"speaker":"A"},{"text":"command","start":1712180,"end":1712500,"confidence":0.9995117,"speaker":"A"},{"text":"you","start":1712500,"end":1712660,"confidence":0.9970703,"speaker":"A"},{"text":"need","start":1712660,"end":1712820,"confidence":0.99902344,"speaker":"A"},{"text":"to","start":1712820,"end":1712980,"confidence":1,"speaker":"A"},{"text":"run","start":1712980,"end":1713260,"confidence":0.9995117,"speaker":"A"},{"text":"and","start":1713340,"end":1713620,"confidence":0.99853516,"speaker":"A"},{"text":"then","start":1713620,"end":1713780,"confidence":0.9946289,"speaker":"A"},{"text":"you","start":1713780,"end":1713940,"confidence":0.99853516,"speaker":"A"},{"text":"just","start":1713940,"end":1714099,"confidence":0.9995117,"speaker":"A"},{"text":"paste","start":1714099,"end":1714420,"confidence":0.98950195,"speaker":"A"},{"text":"in","start":1714420,"end":1714580,"confidence":0.9951172,"speaker":"A"},{"text":"the","start":1714580,"end":1714700,"confidence":0.9995117,"speaker":"A"},{"text":"public","start":1714700,"end":1714900,"confidence":0.9995117,"speaker":"A"},{"text":"key","start":1714900,"end":1715180,"confidence":0.9995117,"speaker":"A"},{"text":"in","start":1715180,"end":1715380,"confidence":0.9169922,"speaker":"A"},{"text":"here.","start":1715380,"end":1715660,"confidence":0.9995117,"speaker":"A"},{"text":"That","start":1716380,"end":1716700,"confidence":0.9980469,"speaker":"A"},{"text":"gives","start":1716700,"end":1717060,"confidence":0.9995117,"speaker":"A"},{"text":"you.","start":1717060,"end":1717340,"confidence":0.9995117,"speaker":"A"},{"text":"That","start":1718780,"end":1719060,"confidence":0.8378906,"speaker":"A"},{"text":"will","start":1719060,"end":1719220,"confidence":0.9951172,"speaker":"A"},{"text":"give","start":1719220,"end":1719380,"confidence":1,"speaker":"A"},{"text":"you","start":1719380,"end":1719540,"confidence":1,"speaker":"A"},{"text":"everything","start":1719540,"end":1719780,"confidence":0.9995117,"speaker":"A"},{"text":"you","start":1719780,"end":1720020,"confidence":0.99902344,"speaker":"A"},{"text":"need.","start":1720020,"end":1720300,"confidence":0.9995117,"speaker":"A"}]},{"text":"So here's how to run it. Basically, sorry about that.","start":1720860,"end":1724630,"confidence":0.9995117,"words":[{"text":"So","start":1720860,"end":1721140,"confidence":0.9995117,"speaker":"A"},{"text":"here's","start":1721140,"end":1721540,"confidence":0.9949544,"speaker":"A"},{"text":"how","start":1721540,"end":1721780,"confidence":1,"speaker":"A"},{"text":"to","start":1721780,"end":1721940,"confidence":0.9995117,"speaker":"A"},{"text":"run","start":1721940,"end":1722100,"confidence":1,"speaker":"A"},{"text":"it.","start":1722100,"end":1722300,"confidence":0.99902344,"speaker":"A"},{"text":"Basically,","start":1722300,"end":1722780,"confidence":0.998291,"speaker":"A"},{"text":"sorry","start":1723990,"end":1724190,"confidence":0.9773763,"speaker":"A"},{"text":"about","start":1724190,"end":1724350,"confidence":0.9819336,"speaker":"A"},{"text":"that.","start":1724350,"end":1724630,"confidence":0.9941406,"speaker":"A"}]},{"text":"We just run that. That gives us the key. We can go ahead and get the public key. We can also pipe it to PB Copy and then all we have to do is paste that in the box over here.","start":1737190,"end":1750930,"confidence":0.7998047,"words":[{"text":"We","start":1737190,"end":1737470,"confidence":0.7998047,"speaker":"A"},{"text":"just","start":1737470,"end":1737670,"confidence":0.99853516,"speaker":"A"},{"text":"run","start":1737670,"end":1737870,"confidence":0.9975586,"speaker":"A"},{"text":"that.","start":1737870,"end":1738150,"confidence":0.9970703,"speaker":"A"},{"text":"That","start":1738470,"end":1738750,"confidence":0.9995117,"speaker":"A"},{"text":"gives","start":1738750,"end":1738950,"confidence":0.99975586,"speaker":"A"},{"text":"us","start":1738950,"end":1739070,"confidence":1,"speaker":"A"},{"text":"the","start":1739070,"end":1739230,"confidence":0.9995117,"speaker":"A"},{"text":"key.","start":1739230,"end":1739510,"confidence":0.9995117,"speaker":"A"},{"text":"We","start":1740710,"end":1740990,"confidence":0.99902344,"speaker":"A"},{"text":"can","start":1740990,"end":1741150,"confidence":0.9995117,"speaker":"A"},{"text":"go","start":1741150,"end":1741310,"confidence":0.99902344,"speaker":"A"},{"text":"ahead","start":1741310,"end":1741550,"confidence":0.9995117,"speaker":"A"},{"text":"and","start":1741550,"end":1741910,"confidence":0.9970703,"speaker":"A"},{"text":"get","start":1742070,"end":1742350,"confidence":1,"speaker":"A"},{"text":"the","start":1742350,"end":1742510,"confidence":1,"speaker":"A"},{"text":"public","start":1742510,"end":1742750,"confidence":1,"speaker":"A"},{"text":"key.","start":1742750,"end":1743110,"confidence":0.9995117,"speaker":"A"},{"text":"We","start":1743190,"end":1743470,"confidence":0.9980469,"speaker":"A"},{"text":"can","start":1743470,"end":1743750,"confidence":0.9995117,"speaker":"A"},{"text":"also","start":1743910,"end":1744270,"confidence":0.99902344,"speaker":"A"},{"text":"pipe","start":1744270,"end":1744670,"confidence":0.9607747,"speaker":"A"},{"text":"it","start":1744670,"end":1744870,"confidence":0.9975586,"speaker":"A"},{"text":"to","start":1744870,"end":1745070,"confidence":0.9975586,"speaker":"A"},{"text":"PB","start":1745070,"end":1745390,"confidence":0.79541016,"speaker":"A"},{"text":"Copy","start":1745390,"end":1745990,"confidence":0.9637044,"speaker":"A"},{"text":"and","start":1746470,"end":1746750,"confidence":0.9321289,"speaker":"A"},{"text":"then","start":1746750,"end":1746910,"confidence":0.98779297,"speaker":"A"},{"text":"all","start":1746910,"end":1747070,"confidence":0.9995117,"speaker":"A"},{"text":"we","start":1747070,"end":1747190,"confidence":0.9995117,"speaker":"A"},{"text":"have","start":1747190,"end":1747310,"confidence":0.95947266,"speaker":"A"},{"text":"to","start":1747310,"end":1747430,"confidence":0.99609375,"speaker":"A"},{"text":"do","start":1747430,"end":1747590,"confidence":0.99609375,"speaker":"A"},{"text":"is","start":1747590,"end":1747830,"confidence":0.99902344,"speaker":"A"},{"text":"paste","start":1747830,"end":1748110,"confidence":0.9172363,"speaker":"A"},{"text":"that","start":1748110,"end":1748310,"confidence":0.99560547,"speaker":"A"},{"text":"in","start":1748310,"end":1748510,"confidence":0.9970703,"speaker":"A"},{"text":"the","start":1748510,"end":1748670,"confidence":0.99853516,"speaker":"A"},{"text":"box","start":1748670,"end":1749030,"confidence":0.99780273,"speaker":"A"},{"text":"over","start":1750370,"end":1750570,"confidence":0.9951172,"speaker":"A"},{"text":"here.","start":1750570,"end":1750930,"confidence":0.9995117,"speaker":"A"}]},{"text":"There we go.","start":1757970,"end":1758690,"confidence":0.98046875,"words":[{"text":"There","start":1757970,"end":1758250,"confidence":0.98046875,"speaker":"A"},{"text":"we","start":1758250,"end":1758410,"confidence":0.5283203,"speaker":"A"},{"text":"go.","start":1758410,"end":1758690,"confidence":1,"speaker":"A"}]},{"text":"It's pretty complicated to use the server key. We can spell on the miskit code on how to do it because it does a lot of that work for you if you have it. But you will need the, the private key, the key id, I think, I think that's it. And then you should be good with having access now to the public database. So just to go over, there's differences between the public and private database.","start":1765890,"end":1795490,"confidence":0.9930013,"words":[{"text":"It's","start":1765890,"end":1766250,"confidence":0.9930013,"speaker":"A"},{"text":"pretty","start":1766250,"end":1766570,"confidence":0.9998372,"speaker":"A"},{"text":"complicated","start":1766570,"end":1767250,"confidence":1,"speaker":"A"},{"text":"to","start":1767250,"end":1767490,"confidence":0.9995117,"speaker":"A"},{"text":"use","start":1767490,"end":1767770,"confidence":1,"speaker":"A"},{"text":"the","start":1767770,"end":1768010,"confidence":0.9995117,"speaker":"A"},{"text":"server","start":1768010,"end":1768450,"confidence":0.99975586,"speaker":"A"},{"text":"key.","start":1768450,"end":1768770,"confidence":0.99560547,"speaker":"A"},{"text":"We","start":1770050,"end":1770330,"confidence":0.9951172,"speaker":"A"},{"text":"can","start":1770330,"end":1770490,"confidence":0.99902344,"speaker":"A"},{"text":"spell","start":1770490,"end":1770770,"confidence":0.9838867,"speaker":"A"},{"text":"on","start":1770770,"end":1771050,"confidence":0.8208008,"speaker":"A"},{"text":"the","start":1771050,"end":1771250,"confidence":0.99658203,"speaker":"A"},{"text":"miskit","start":1771250,"end":1771690,"confidence":0.9238281,"speaker":"A"},{"text":"code","start":1771690,"end":1771970,"confidence":0.99348956,"speaker":"A"},{"text":"on","start":1771970,"end":1772090,"confidence":0.9975586,"speaker":"A"},{"text":"how","start":1772090,"end":1772250,"confidence":0.9995117,"speaker":"A"},{"text":"to","start":1772250,"end":1772410,"confidence":0.99902344,"speaker":"A"},{"text":"do","start":1772410,"end":1772570,"confidence":0.9995117,"speaker":"A"},{"text":"it","start":1772570,"end":1772850,"confidence":0.9995117,"speaker":"A"},{"text":"because","start":1773170,"end":1773450,"confidence":0.9663086,"speaker":"A"},{"text":"it","start":1773450,"end":1773610,"confidence":0.9995117,"speaker":"A"},{"text":"does","start":1773610,"end":1773810,"confidence":0.99902344,"speaker":"A"},{"text":"a","start":1773810,"end":1773970,"confidence":0.9995117,"speaker":"A"},{"text":"lot","start":1773970,"end":1774050,"confidence":1,"speaker":"A"},{"text":"of","start":1774050,"end":1774130,"confidence":0.9980469,"speaker":"A"},{"text":"that","start":1774130,"end":1774290,"confidence":0.99560547,"speaker":"A"},{"text":"work","start":1774290,"end":1774530,"confidence":1,"speaker":"A"},{"text":"for","start":1774530,"end":1774730,"confidence":0.99902344,"speaker":"A"},{"text":"you","start":1774730,"end":1774930,"confidence":0.9995117,"speaker":"A"},{"text":"if","start":1774930,"end":1775170,"confidence":0.59228516,"speaker":"A"},{"text":"you","start":1775170,"end":1775330,"confidence":0.9995117,"speaker":"A"},{"text":"have","start":1775330,"end":1775450,"confidence":0.9995117,"speaker":"A"},{"text":"it.","start":1775450,"end":1775730,"confidence":0.9916992,"speaker":"A"},{"text":"But","start":1776610,"end":1776730,"confidence":0.99121094,"speaker":"A"},{"text":"you","start":1776730,"end":1776890,"confidence":0.9995117,"speaker":"A"},{"text":"will","start":1776890,"end":1777090,"confidence":0.9995117,"speaker":"A"},{"text":"need","start":1777090,"end":1777410,"confidence":0.9995117,"speaker":"A"},{"text":"the,","start":1777650,"end":1778050,"confidence":0.8984375,"speaker":"A"},{"text":"the","start":1779170,"end":1779490,"confidence":0.98876953,"speaker":"A"},{"text":"private","start":1779490,"end":1779810,"confidence":0.9995117,"speaker":"A"},{"text":"key,","start":1779890,"end":1780290,"confidence":0.9975586,"speaker":"A"},{"text":"the","start":1780290,"end":1780570,"confidence":0.99121094,"speaker":"A"},{"text":"key","start":1780570,"end":1780810,"confidence":0.9946289,"speaker":"A"},{"text":"id,","start":1780810,"end":1781170,"confidence":0.98583984,"speaker":"A"},{"text":"I","start":1782290,"end":1782570,"confidence":0.90771484,"speaker":"A"},{"text":"think,","start":1782570,"end":1782850,"confidence":0.9980469,"speaker":"A"},{"text":"I","start":1783170,"end":1783450,"confidence":0.8652344,"speaker":"A"},{"text":"think","start":1783450,"end":1783610,"confidence":0.9868164,"speaker":"A"},{"text":"that's","start":1783610,"end":1783810,"confidence":0.9995117,"speaker":"A"},{"text":"it.","start":1783810,"end":1784050,"confidence":0.9941406,"speaker":"A"},{"text":"And","start":1784370,"end":1784650,"confidence":0.9921875,"speaker":"A"},{"text":"then","start":1784650,"end":1784890,"confidence":0.94677734,"speaker":"A"},{"text":"you","start":1784890,"end":1785130,"confidence":0.99658203,"speaker":"A"},{"text":"should","start":1785130,"end":1785290,"confidence":1,"speaker":"A"},{"text":"be","start":1785290,"end":1785490,"confidence":1,"speaker":"A"},{"text":"good","start":1785490,"end":1785810,"confidence":0.9995117,"speaker":"A"},{"text":"with","start":1786130,"end":1786490,"confidence":0.9975586,"speaker":"A"},{"text":"having","start":1786490,"end":1786810,"confidence":0.9555664,"speaker":"A"},{"text":"access","start":1786810,"end":1787170,"confidence":1,"speaker":"A"},{"text":"now","start":1787170,"end":1787490,"confidence":0.9975586,"speaker":"A"},{"text":"to","start":1787490,"end":1787770,"confidence":0.9995117,"speaker":"A"},{"text":"the","start":1787770,"end":1788010,"confidence":0.9995117,"speaker":"A"},{"text":"public","start":1788010,"end":1788290,"confidence":0.9995117,"speaker":"A"},{"text":"database.","start":1789330,"end":1790130,"confidence":0.99902344,"speaker":"A"},{"text":"So","start":1790850,"end":1791250,"confidence":0.98876953,"speaker":"A"},{"text":"just","start":1791570,"end":1791889,"confidence":0.9995117,"speaker":"A"},{"text":"to","start":1791889,"end":1792050,"confidence":0.99853516,"speaker":"A"},{"text":"go","start":1792050,"end":1792209,"confidence":0.99902344,"speaker":"A"},{"text":"over,","start":1792209,"end":1792530,"confidence":1,"speaker":"A"},{"text":"there's","start":1792610,"end":1793050,"confidence":0.9892578,"speaker":"A"},{"text":"differences","start":1793050,"end":1793450,"confidence":0.9995117,"speaker":"A"},{"text":"between","start":1793450,"end":1793770,"confidence":1,"speaker":"A"},{"text":"the","start":1793770,"end":1793970,"confidence":0.9995117,"speaker":"A"},{"text":"public","start":1793970,"end":1794210,"confidence":1,"speaker":"A"},{"text":"and","start":1794210,"end":1794490,"confidence":0.99902344,"speaker":"A"},{"text":"private","start":1794490,"end":1794730,"confidence":1,"speaker":"A"},{"text":"database.","start":1794730,"end":1795490,"confidence":0.99820966,"speaker":"A"}]},{"text":"So this is query. You can see my cursor, right? Query and lookup of records is available on all but file changes or, excuse me, record changes. It's not available on public zones, aren't really available in public zone changes aren't available in public notifications. Zone notifications aren't available in public, but query notifications are.","start":1797170,"end":1821990,"confidence":0.99609375,"words":[{"text":"So","start":1797170,"end":1797570,"confidence":0.99609375,"speaker":"A"},{"text":"this","start":1797730,"end":1798050,"confidence":0.9995117,"speaker":"A"},{"text":"is","start":1798050,"end":1798370,"confidence":0.9995117,"speaker":"A"},{"text":"query.","start":1798530,"end":1799090,"confidence":0.9975586,"speaker":"A"},{"text":"You","start":1799570,"end":1799810,"confidence":0.9995117,"speaker":"A"},{"text":"can","start":1799810,"end":1799930,"confidence":0.5439453,"speaker":"A"},{"text":"see","start":1799930,"end":1800090,"confidence":0.99609375,"speaker":"A"},{"text":"my","start":1800090,"end":1800250,"confidence":0.8847656,"speaker":"A"},{"text":"cursor,","start":1800250,"end":1800650,"confidence":0.9938151,"speaker":"A"},{"text":"right?","start":1800650,"end":1800930,"confidence":0.97265625,"speaker":"A"},{"text":"Query","start":1800930,"end":1801330,"confidence":0.9904785,"speaker":"A"},{"text":"and","start":1801330,"end":1801530,"confidence":0.53759766,"speaker":"A"},{"text":"lookup","start":1801530,"end":1802010,"confidence":0.94018555,"speaker":"A"},{"text":"of","start":1802010,"end":1802330,"confidence":0.9916992,"speaker":"A"},{"text":"records","start":1802330,"end":1803010,"confidence":0.99975586,"speaker":"A"},{"text":"is","start":1803010,"end":1803290,"confidence":0.9995117,"speaker":"A"},{"text":"available","start":1803290,"end":1803570,"confidence":0.9995117,"speaker":"A"},{"text":"on","start":1803650,"end":1803970,"confidence":0.99853516,"speaker":"A"},{"text":"all","start":1803970,"end":1804290,"confidence":0.99658203,"speaker":"A"},{"text":"but","start":1805270,"end":1805510,"confidence":0.9897461,"speaker":"A"},{"text":"file","start":1805590,"end":1806030,"confidence":0.9970703,"speaker":"A"},{"text":"changes","start":1806030,"end":1806630,"confidence":0.9992676,"speaker":"A"},{"text":"or,","start":1806790,"end":1807110,"confidence":0.97314453,"speaker":"A"},{"text":"excuse","start":1807110,"end":1807430,"confidence":0.99820966,"speaker":"A"},{"text":"me,","start":1807430,"end":1807670,"confidence":0.9995117,"speaker":"A"},{"text":"record","start":1807990,"end":1808350,"confidence":0.99609375,"speaker":"A"},{"text":"changes.","start":1808350,"end":1808830,"confidence":0.99975586,"speaker":"A"},{"text":"It's","start":1808830,"end":1809070,"confidence":0.8819987,"speaker":"A"},{"text":"not","start":1809070,"end":1809230,"confidence":1,"speaker":"A"},{"text":"available","start":1809230,"end":1809510,"confidence":0.99853516,"speaker":"A"},{"text":"on","start":1809830,"end":1810150,"confidence":0.9160156,"speaker":"A"},{"text":"public","start":1810150,"end":1810470,"confidence":0.9995117,"speaker":"A"},{"text":"zones,","start":1810950,"end":1811390,"confidence":0.9909668,"speaker":"A"},{"text":"aren't","start":1811390,"end":1811670,"confidence":0.9958496,"speaker":"A"},{"text":"really","start":1811670,"end":1811830,"confidence":1,"speaker":"A"},{"text":"available","start":1811830,"end":1812150,"confidence":0.99853516,"speaker":"A"},{"text":"in","start":1812150,"end":1812430,"confidence":0.9394531,"speaker":"A"},{"text":"public","start":1812430,"end":1812710,"confidence":1,"speaker":"A"},{"text":"zone","start":1812790,"end":1813190,"confidence":0.96240234,"speaker":"A"},{"text":"changes","start":1813190,"end":1813550,"confidence":0.8989258,"speaker":"A"},{"text":"aren't","start":1813550,"end":1813870,"confidence":0.9959717,"speaker":"A"},{"text":"available","start":1813870,"end":1814150,"confidence":1,"speaker":"A"},{"text":"in","start":1814470,"end":1814750,"confidence":0.9667969,"speaker":"A"},{"text":"public","start":1814750,"end":1815030,"confidence":1,"speaker":"A"},{"text":"notifications.","start":1815670,"end":1816470,"confidence":0.9949544,"speaker":"A"},{"text":"Zone","start":1816550,"end":1816950,"confidence":0.94677734,"speaker":"A"},{"text":"notifications","start":1816950,"end":1817630,"confidence":0.9996745,"speaker":"A"},{"text":"aren't","start":1817630,"end":1817950,"confidence":0.9765625,"speaker":"A"},{"text":"available","start":1817950,"end":1818230,"confidence":1,"speaker":"A"},{"text":"in","start":1818310,"end":1818590,"confidence":0.9941406,"speaker":"A"},{"text":"public,","start":1818590,"end":1818870,"confidence":1,"speaker":"A"},{"text":"but","start":1819670,"end":1820070,"confidence":0.9921875,"speaker":"A"},{"text":"query","start":1820070,"end":1820550,"confidence":0.82421875,"speaker":"A"},{"text":"notifications","start":1820709,"end":1821510,"confidence":0.9996745,"speaker":"A"},{"text":"are.","start":1821590,"end":1821990,"confidence":0.9902344,"speaker":"A"}]},{"text":"And you can also do any stuff with assets which are basically binary files. You can also do that in all of them. You can't do query notifications on shared. Shared would essentially work like private essentially. So it's just a matter of who.","start":1821990,"end":1840530,"confidence":0.9921875,"words":[{"text":"And","start":1821990,"end":1822390,"confidence":0.9921875,"speaker":"A"},{"text":"you","start":1822390,"end":1822630,"confidence":0.9995117,"speaker":"A"},{"text":"can","start":1822630,"end":1822750,"confidence":0.9995117,"speaker":"A"},{"text":"also","start":1822750,"end":1822990,"confidence":0.9995117,"speaker":"A"},{"text":"do","start":1822990,"end":1823350,"confidence":1,"speaker":"A"},{"text":"any","start":1823350,"end":1823750,"confidence":0.99853516,"speaker":"A"},{"text":"stuff","start":1823750,"end":1824150,"confidence":0.9996745,"speaker":"A"},{"text":"with","start":1824150,"end":1824470,"confidence":0.98876953,"speaker":"A"},{"text":"assets","start":1824710,"end":1825270,"confidence":0.7792969,"speaker":"A"},{"text":"which","start":1825350,"end":1825630,"confidence":0.99853516,"speaker":"A"},{"text":"are","start":1825630,"end":1825790,"confidence":1,"speaker":"A"},{"text":"basically","start":1825790,"end":1826190,"confidence":0.99975586,"speaker":"A"},{"text":"binary","start":1826190,"end":1826710,"confidence":0.9995117,"speaker":"A"},{"text":"files.","start":1826710,"end":1827030,"confidence":0.99194336,"speaker":"A"},{"text":"You","start":1827030,"end":1827190,"confidence":0.99853516,"speaker":"A"},{"text":"can","start":1827190,"end":1827310,"confidence":0.99853516,"speaker":"A"},{"text":"also","start":1827310,"end":1827470,"confidence":0.9995117,"speaker":"A"},{"text":"do","start":1827470,"end":1827630,"confidence":0.9995117,"speaker":"A"},{"text":"that","start":1827630,"end":1827910,"confidence":0.99902344,"speaker":"A"},{"text":"in","start":1828310,"end":1828670,"confidence":0.5600586,"speaker":"A"},{"text":"all","start":1828670,"end":1828910,"confidence":0.9995117,"speaker":"A"},{"text":"of","start":1828910,"end":1829070,"confidence":0.99902344,"speaker":"A"},{"text":"them.","start":1829070,"end":1829350,"confidence":0.9145508,"speaker":"A"},{"text":"You","start":1830630,"end":1830910,"confidence":0.99658203,"speaker":"A"},{"text":"can't","start":1830910,"end":1831230,"confidence":0.9586589,"speaker":"A"},{"text":"do","start":1831230,"end":1831590,"confidence":1,"speaker":"A"},{"text":"query","start":1831750,"end":1832190,"confidence":0.970459,"speaker":"A"},{"text":"notifications","start":1832190,"end":1832990,"confidence":0.99934894,"speaker":"A"},{"text":"on","start":1832990,"end":1833270,"confidence":0.98046875,"speaker":"A"},{"text":"shared.","start":1833270,"end":1833830,"confidence":0.99780273,"speaker":"A"},{"text":"Shared","start":1834470,"end":1834910,"confidence":0.9873047,"speaker":"A"},{"text":"would","start":1834910,"end":1835110,"confidence":0.5698242,"speaker":"A"},{"text":"essentially","start":1835110,"end":1835590,"confidence":0.99902344,"speaker":"A"},{"text":"work","start":1835590,"end":1835870,"confidence":1,"speaker":"A"},{"text":"like","start":1835870,"end":1836110,"confidence":0.9980469,"speaker":"A"},{"text":"private","start":1836110,"end":1836390,"confidence":0.99902344,"speaker":"A"},{"text":"essentially.","start":1836850,"end":1837410,"confidence":0.9968262,"speaker":"A"},{"text":"So","start":1837490,"end":1837890,"confidence":0.9946289,"speaker":"A"},{"text":"it's","start":1839090,"end":1839410,"confidence":0.9995117,"speaker":"A"},{"text":"just","start":1839410,"end":1839530,"confidence":0.9995117,"speaker":"A"},{"text":"a","start":1839530,"end":1839650,"confidence":0.9995117,"speaker":"A"},{"text":"matter","start":1839650,"end":1839810,"confidence":1,"speaker":"A"},{"text":"of","start":1839810,"end":1840130,"confidence":0.99902344,"speaker":"A"},{"text":"who.","start":1840130,"end":1840530,"confidence":0.77685547,"speaker":"A"}]},{"text":"Who's the owner and how is it shared.","start":1840530,"end":1842610,"confidence":0.9977214,"words":[{"text":"Who's","start":1840530,"end":1840930,"confidence":0.9977214,"speaker":"A"},{"text":"the","start":1840930,"end":1841050,"confidence":0.99853516,"speaker":"A"},{"text":"owner","start":1841050,"end":1841370,"confidence":1,"speaker":"A"},{"text":"and","start":1841370,"end":1841570,"confidence":0.99609375,"speaker":"A"},{"text":"how","start":1841570,"end":1841810,"confidence":0.9995117,"speaker":"A"},{"text":"is","start":1841810,"end":1841970,"confidence":0.94970703,"speaker":"A"},{"text":"it","start":1841970,"end":1842090,"confidence":0.99902344,"speaker":"A"},{"text":"shared.","start":1842090,"end":1842610,"confidence":0.9968262,"speaker":"A"}]},{"text":"So one of the big challenges I think we've all faced this when we've dealt with certain web services is field type polymorphism. If you've done JSON where you don't know what type you're getting back or what data you're getting back, this can Be a bit challenging. So if you look at the documentation in Web Services Reference, there is a, there's a page called types and dictionaries and there is types. There's different type values for each field. If you're familiar with CloudKit, you've seen this, right?","start":1844690,"end":1878450,"confidence":0.99658203,"words":[{"text":"So","start":1844690,"end":1844930,"confidence":0.99658203,"speaker":"A"},{"text":"one","start":1844930,"end":1845050,"confidence":0.9794922,"speaker":"A"},{"text":"of","start":1845050,"end":1845210,"confidence":1,"speaker":"A"},{"text":"the","start":1845210,"end":1845450,"confidence":0.9995117,"speaker":"A"},{"text":"big","start":1845450,"end":1845730,"confidence":1,"speaker":"A"},{"text":"challenges","start":1845730,"end":1846370,"confidence":0.96468097,"speaker":"A"},{"text":"I","start":1846450,"end":1846730,"confidence":0.99853516,"speaker":"A"},{"text":"think","start":1846730,"end":1846890,"confidence":1,"speaker":"A"},{"text":"we've","start":1846890,"end":1847170,"confidence":0.9977214,"speaker":"A"},{"text":"all","start":1847170,"end":1847330,"confidence":0.9995117,"speaker":"A"},{"text":"faced","start":1847330,"end":1847650,"confidence":0.95825195,"speaker":"A"},{"text":"this","start":1847650,"end":1847810,"confidence":0.99072266,"speaker":"A"},{"text":"when","start":1847810,"end":1848010,"confidence":0.99609375,"speaker":"A"},{"text":"we've","start":1848010,"end":1848370,"confidence":0.98095703,"speaker":"A"},{"text":"dealt","start":1848370,"end":1848650,"confidence":0.9992676,"speaker":"A"},{"text":"with","start":1848650,"end":1848810,"confidence":1,"speaker":"A"},{"text":"certain","start":1848810,"end":1849010,"confidence":0.9995117,"speaker":"A"},{"text":"web","start":1849010,"end":1849290,"confidence":0.99902344,"speaker":"A"},{"text":"services","start":1849290,"end":1849570,"confidence":0.9995117,"speaker":"A"},{"text":"is","start":1850530,"end":1850930,"confidence":0.98876953,"speaker":"A"},{"text":"field","start":1851410,"end":1851810,"confidence":0.9897461,"speaker":"A"},{"text":"type","start":1851970,"end":1852449,"confidence":0.810791,"speaker":"A"},{"text":"polymorphism.","start":1852449,"end":1853370,"confidence":0.9991862,"speaker":"A"},{"text":"If","start":1853370,"end":1853570,"confidence":1,"speaker":"A"},{"text":"you've","start":1853570,"end":1853730,"confidence":0.9998372,"speaker":"A"},{"text":"done","start":1853730,"end":1853890,"confidence":0.9975586,"speaker":"A"},{"text":"JSON","start":1853890,"end":1854370,"confidence":0.7998047,"speaker":"A"},{"text":"where","start":1854370,"end":1854650,"confidence":0.87939453,"speaker":"A"},{"text":"you","start":1854650,"end":1854850,"confidence":1,"speaker":"A"},{"text":"don't","start":1854850,"end":1855090,"confidence":0.9996745,"speaker":"A"},{"text":"know","start":1855090,"end":1855210,"confidence":0.99902344,"speaker":"A"},{"text":"what","start":1855210,"end":1855370,"confidence":0.9995117,"speaker":"A"},{"text":"type","start":1855370,"end":1855730,"confidence":0.9946289,"speaker":"A"},{"text":"you're","start":1855730,"end":1855970,"confidence":1,"speaker":"A"},{"text":"getting","start":1855970,"end":1856130,"confidence":0.9995117,"speaker":"A"},{"text":"back","start":1856130,"end":1856370,"confidence":0.9980469,"speaker":"A"},{"text":"or","start":1856370,"end":1856570,"confidence":0.9980469,"speaker":"A"},{"text":"what","start":1856570,"end":1856730,"confidence":0.98876953,"speaker":"A"},{"text":"data","start":1856730,"end":1856930,"confidence":0.9980469,"speaker":"A"},{"text":"you're","start":1856930,"end":1857170,"confidence":0.9995117,"speaker":"A"},{"text":"getting","start":1857170,"end":1857370,"confidence":0.9916992,"speaker":"A"},{"text":"back,","start":1857370,"end":1857730,"confidence":0.9526367,"speaker":"A"},{"text":"this","start":1858050,"end":1858330,"confidence":0.9995117,"speaker":"A"},{"text":"can","start":1858330,"end":1858490,"confidence":0.99902344,"speaker":"A"},{"text":"Be","start":1858490,"end":1858610,"confidence":1,"speaker":"A"},{"text":"a","start":1858610,"end":1858690,"confidence":0.9995117,"speaker":"A"},{"text":"bit","start":1858690,"end":1858850,"confidence":0.99902344,"speaker":"A"},{"text":"challenging.","start":1858850,"end":1859410,"confidence":0.9601237,"speaker":"A"},{"text":"So","start":1860530,"end":1860930,"confidence":0.9951172,"speaker":"A"},{"text":"if","start":1861730,"end":1862050,"confidence":0.6791992,"speaker":"A"},{"text":"you","start":1862050,"end":1862250,"confidence":1,"speaker":"A"},{"text":"look","start":1862250,"end":1862410,"confidence":1,"speaker":"A"},{"text":"at","start":1862410,"end":1862610,"confidence":0.9995117,"speaker":"A"},{"text":"the","start":1862610,"end":1862850,"confidence":0.9980469,"speaker":"A"},{"text":"documentation","start":1862850,"end":1863650,"confidence":0.99902344,"speaker":"A"},{"text":"in","start":1864290,"end":1864490,"confidence":0.78466797,"speaker":"A"},{"text":"Web","start":1864490,"end":1864810,"confidence":0.9890137,"speaker":"A"},{"text":"Services","start":1864810,"end":1865090,"confidence":0.99902344,"speaker":"A"},{"text":"Reference,","start":1865090,"end":1865810,"confidence":0.9918213,"speaker":"A"},{"text":"there","start":1866850,"end":1867210,"confidence":0.9921875,"speaker":"A"},{"text":"is","start":1867210,"end":1867570,"confidence":0.99902344,"speaker":"A"},{"text":"a,","start":1867890,"end":1868290,"confidence":0.99853516,"speaker":"A"},{"text":"there's","start":1869090,"end":1869610,"confidence":0.9824219,"speaker":"A"},{"text":"a","start":1869610,"end":1869890,"confidence":0.99902344,"speaker":"A"},{"text":"page","start":1869890,"end":1870290,"confidence":0.9951172,"speaker":"A"},{"text":"called","start":1870290,"end":1870530,"confidence":0.9995117,"speaker":"A"},{"text":"types","start":1870530,"end":1870810,"confidence":0.87719727,"speaker":"A"},{"text":"and","start":1870810,"end":1870970,"confidence":0.9536133,"speaker":"A"},{"text":"dictionaries","start":1870970,"end":1871650,"confidence":0.99609375,"speaker":"A"},{"text":"and","start":1871650,"end":1872010,"confidence":0.99902344,"speaker":"A"},{"text":"there","start":1872010,"end":1872290,"confidence":0.9980469,"speaker":"A"},{"text":"is","start":1872290,"end":1872610,"confidence":0.99609375,"speaker":"A"},{"text":"types.","start":1872610,"end":1873170,"confidence":0.9255371,"speaker":"A"},{"text":"There's","start":1874050,"end":1874410,"confidence":0.98860675,"speaker":"A"},{"text":"different","start":1874410,"end":1874610,"confidence":1,"speaker":"A"},{"text":"type","start":1874610,"end":1875010,"confidence":0.83618164,"speaker":"A"},{"text":"values","start":1875010,"end":1875530,"confidence":0.9992676,"speaker":"A"},{"text":"for","start":1875530,"end":1875690,"confidence":1,"speaker":"A"},{"text":"each","start":1875690,"end":1875930,"confidence":1,"speaker":"A"},{"text":"field.","start":1875930,"end":1876250,"confidence":1,"speaker":"A"},{"text":"If","start":1876250,"end":1876450,"confidence":1,"speaker":"A"},{"text":"you're","start":1876450,"end":1876610,"confidence":1,"speaker":"A"},{"text":"familiar","start":1876610,"end":1876890,"confidence":1,"speaker":"A"},{"text":"with","start":1876890,"end":1877050,"confidence":0.9995117,"speaker":"A"},{"text":"CloudKit,","start":1877050,"end":1877530,"confidence":0.953125,"speaker":"A"},{"text":"you've","start":1877530,"end":1877730,"confidence":0.99886066,"speaker":"A"},{"text":"seen","start":1877730,"end":1877890,"confidence":0.9995117,"speaker":"A"},{"text":"this,","start":1877890,"end":1878130,"confidence":0.9980469,"speaker":"A"},{"text":"right?","start":1878130,"end":1878450,"confidence":0.99853516,"speaker":"A"}]},{"text":"So you have an asset which is basically a, a binary file. You have bytes which is essentially a 60 byte base 64 encoded string, date type which is returned as a number. Double is returned as a number because These are the JavaScript types. Int is returned as a number and then there's location reference and then string and list. And how would you like, how do you do adjacent object like this?","start":1879170,"end":1916620,"confidence":0.9995117,"words":[{"text":"So","start":1879170,"end":1879570,"confidence":0.9995117,"speaker":"A"},{"text":"you","start":1879570,"end":1879850,"confidence":0.9995117,"speaker":"A"},{"text":"have","start":1879850,"end":1880089,"confidence":1,"speaker":"A"},{"text":"an","start":1880089,"end":1880329,"confidence":0.99853516,"speaker":"A"},{"text":"asset","start":1880329,"end":1880650,"confidence":0.9995117,"speaker":"A"},{"text":"which","start":1880650,"end":1880850,"confidence":1,"speaker":"A"},{"text":"is","start":1880850,"end":1881050,"confidence":0.9995117,"speaker":"A"},{"text":"basically","start":1881050,"end":1881490,"confidence":1,"speaker":"A"},{"text":"a,","start":1882210,"end":1882610,"confidence":0.9838867,"speaker":"A"},{"text":"a","start":1884290,"end":1884690,"confidence":0.9995117,"speaker":"A"},{"text":"binary","start":1884690,"end":1885330,"confidence":0.9998372,"speaker":"A"},{"text":"file.","start":1885330,"end":1885810,"confidence":0.69873047,"speaker":"A"},{"text":"You","start":1886850,"end":1887170,"confidence":1,"speaker":"A"},{"text":"have","start":1887170,"end":1887490,"confidence":1,"speaker":"A"},{"text":"bytes","start":1887490,"end":1888210,"confidence":0.8411458,"speaker":"A"},{"text":"which","start":1889090,"end":1889410,"confidence":1,"speaker":"A"},{"text":"is","start":1889410,"end":1889650,"confidence":0.9995117,"speaker":"A"},{"text":"essentially","start":1889650,"end":1890130,"confidence":0.99902344,"speaker":"A"},{"text":"a","start":1890130,"end":1890450,"confidence":0.95996094,"speaker":"A"},{"text":"60","start":1890530,"end":1890930,"confidence":0.9458,"speaker":"A"},{"text":"byte","start":1891170,"end":1891650,"confidence":0.9658203,"speaker":"A"},{"text":"base","start":1891860,"end":1892100,"confidence":0.8461914,"speaker":"A"},{"text":"64","start":1892100,"end":1892580,"confidence":0.99829,"speaker":"A"},{"text":"encoded","start":1892580,"end":1893140,"confidence":0.9967448,"speaker":"A"},{"text":"string,","start":1893140,"end":1893620,"confidence":0.9970703,"speaker":"A"},{"text":"date","start":1894740,"end":1895140,"confidence":0.98095703,"speaker":"A"},{"text":"type","start":1895140,"end":1895580,"confidence":0.9716797,"speaker":"A"},{"text":"which","start":1895580,"end":1895820,"confidence":0.9995117,"speaker":"A"},{"text":"is","start":1895820,"end":1896060,"confidence":0.99658203,"speaker":"A"},{"text":"returned","start":1896060,"end":1896580,"confidence":0.98876953,"speaker":"A"},{"text":"as","start":1896580,"end":1896700,"confidence":0.99902344,"speaker":"A"},{"text":"a","start":1896700,"end":1896860,"confidence":0.9995117,"speaker":"A"},{"text":"number.","start":1896860,"end":1897140,"confidence":0.99560547,"speaker":"A"},{"text":"Double","start":1897780,"end":1898220,"confidence":0.9511719,"speaker":"A"},{"text":"is","start":1898220,"end":1898460,"confidence":0.98779297,"speaker":"A"},{"text":"returned","start":1898460,"end":1898860,"confidence":0.954834,"speaker":"A"},{"text":"as","start":1898860,"end":1899020,"confidence":0.9951172,"speaker":"A"},{"text":"a","start":1899020,"end":1899140,"confidence":0.99853516,"speaker":"A"},{"text":"number","start":1899140,"end":1899380,"confidence":0.99658203,"speaker":"A"},{"text":"because","start":1899940,"end":1900220,"confidence":0.7080078,"speaker":"A"},{"text":"These","start":1900220,"end":1900380,"confidence":0.99658203,"speaker":"A"},{"text":"are","start":1900380,"end":1900500,"confidence":0.9995117,"speaker":"A"},{"text":"the","start":1900500,"end":1900620,"confidence":0.9995117,"speaker":"A"},{"text":"JavaScript","start":1900620,"end":1901220,"confidence":0.9517415,"speaker":"A"},{"text":"types.","start":1901220,"end":1901620,"confidence":0.76464844,"speaker":"A"},{"text":"Int","start":1902260,"end":1902660,"confidence":0.57714844,"speaker":"A"},{"text":"is","start":1902820,"end":1903220,"confidence":0.99609375,"speaker":"A"},{"text":"returned","start":1903540,"end":1904060,"confidence":0.9616699,"speaker":"A"},{"text":"as","start":1904060,"end":1904220,"confidence":0.99902344,"speaker":"A"},{"text":"a","start":1904220,"end":1904340,"confidence":0.99902344,"speaker":"A"},{"text":"number","start":1904340,"end":1904580,"confidence":0.99609375,"speaker":"A"},{"text":"and","start":1905700,"end":1905980,"confidence":0.9946289,"speaker":"A"},{"text":"then","start":1905980,"end":1906140,"confidence":0.99902344,"speaker":"A"},{"text":"there's","start":1906140,"end":1906420,"confidence":0.85302734,"speaker":"A"},{"text":"location","start":1906420,"end":1906980,"confidence":0.99902344,"speaker":"A"},{"text":"reference","start":1907540,"end":1908260,"confidence":0.8996582,"speaker":"A"},{"text":"and","start":1909300,"end":1909620,"confidence":0.9892578,"speaker":"A"},{"text":"then","start":1909620,"end":1909940,"confidence":0.9980469,"speaker":"A"},{"text":"string","start":1910020,"end":1910500,"confidence":0.9926758,"speaker":"A"},{"text":"and","start":1910500,"end":1910740,"confidence":0.98828125,"speaker":"A"},{"text":"list.","start":1910740,"end":1911060,"confidence":0.99658203,"speaker":"A"},{"text":"And","start":1911620,"end":1912020,"confidence":0.9951172,"speaker":"A"},{"text":"how","start":1912100,"end":1912420,"confidence":0.9980469,"speaker":"A"},{"text":"would","start":1912420,"end":1912620,"confidence":0.94873047,"speaker":"A"},{"text":"you","start":1912620,"end":1912900,"confidence":0.99902344,"speaker":"A"},{"text":"like,","start":1913060,"end":1913420,"confidence":0.9946289,"speaker":"A"},{"text":"how","start":1913420,"end":1913660,"confidence":0.9995117,"speaker":"A"},{"text":"do","start":1913660,"end":1913820,"confidence":0.99658203,"speaker":"A"},{"text":"you","start":1913820,"end":1914020,"confidence":0.9995117,"speaker":"A"},{"text":"do","start":1914020,"end":1914340,"confidence":0.99902344,"speaker":"A"},{"text":"adjacent","start":1914820,"end":1915620,"confidence":0.7462891,"speaker":"A"},{"text":"object","start":1915780,"end":1916220,"confidence":0.82470703,"speaker":"A"},{"text":"like","start":1916220,"end":1916460,"confidence":0.99902344,"speaker":"A"},{"text":"this?","start":1916460,"end":1916620,"confidence":0.99902344,"speaker":"A"}]},{"text":"How would you even represent this in Swift? Because you don't know what type you're going to get. So like I said, this is a work in progress. Sorry. So what I do, I don't know how much you can see this.","start":1916620,"end":1928710,"confidence":0.9975586,"words":[{"text":"How","start":1916620,"end":1916780,"confidence":0.9975586,"speaker":"A"},{"text":"would","start":1916780,"end":1916940,"confidence":0.99560547,"speaker":"A"},{"text":"you","start":1916940,"end":1917100,"confidence":0.9980469,"speaker":"A"},{"text":"even","start":1917100,"end":1917300,"confidence":0.9995117,"speaker":"A"},{"text":"represent","start":1917300,"end":1917620,"confidence":0.99853516,"speaker":"A"},{"text":"this","start":1917620,"end":1917900,"confidence":0.8857422,"speaker":"A"},{"text":"in","start":1917900,"end":1918060,"confidence":0.9404297,"speaker":"A"},{"text":"Swift?","start":1918060,"end":1918380,"confidence":0.9929199,"speaker":"A"},{"text":"Because","start":1918380,"end":1918580,"confidence":0.99902344,"speaker":"A"},{"text":"you","start":1918580,"end":1918740,"confidence":0.9995117,"speaker":"A"},{"text":"don't","start":1918740,"end":1918900,"confidence":0.99934894,"speaker":"A"},{"text":"know","start":1918900,"end":1918980,"confidence":0.99902344,"speaker":"A"},{"text":"what","start":1918980,"end":1919100,"confidence":0.9970703,"speaker":"A"},{"text":"type","start":1919100,"end":1919300,"confidence":0.9980469,"speaker":"A"},{"text":"you're","start":1919300,"end":1919460,"confidence":0.99820966,"speaker":"A"},{"text":"going","start":1919460,"end":1919540,"confidence":0.72802734,"speaker":"A"},{"text":"to","start":1919540,"end":1919620,"confidence":0.99902344,"speaker":"A"},{"text":"get.","start":1919620,"end":1919860,"confidence":0.9980469,"speaker":"A"},{"text":"So","start":1921350,"end":1921590,"confidence":0.9604492,"speaker":"A"},{"text":"like","start":1922790,"end":1923070,"confidence":0.99609375,"speaker":"A"},{"text":"I","start":1923070,"end":1923230,"confidence":0.9995117,"speaker":"A"},{"text":"said,","start":1923230,"end":1923390,"confidence":0.9995117,"speaker":"A"},{"text":"this","start":1923390,"end":1923550,"confidence":0.9995117,"speaker":"A"},{"text":"is","start":1923550,"end":1923710,"confidence":0.9975586,"speaker":"A"},{"text":"a","start":1923710,"end":1923830,"confidence":0.9980469,"speaker":"A"},{"text":"work","start":1923830,"end":1923950,"confidence":0.99853516,"speaker":"A"},{"text":"in","start":1923950,"end":1924110,"confidence":0.99902344,"speaker":"A"},{"text":"progress.","start":1924110,"end":1924510,"confidence":0.99975586,"speaker":"A"},{"text":"Sorry.","start":1924510,"end":1924950,"confidence":0.9889323,"speaker":"A"},{"text":"So","start":1925830,"end":1926150,"confidence":0.94628906,"speaker":"A"},{"text":"what","start":1926150,"end":1926350,"confidence":0.99609375,"speaker":"A"},{"text":"I","start":1926350,"end":1926550,"confidence":0.99853516,"speaker":"A"},{"text":"do,","start":1926550,"end":1926870,"confidence":0.99902344,"speaker":"A"},{"text":"I","start":1927190,"end":1927430,"confidence":0.99853516,"speaker":"A"},{"text":"don't","start":1927430,"end":1927590,"confidence":0.9785156,"speaker":"A"},{"text":"know","start":1927590,"end":1927670,"confidence":0.9975586,"speaker":"A"},{"text":"how","start":1927670,"end":1927790,"confidence":0.99902344,"speaker":"A"},{"text":"much","start":1927790,"end":1927950,"confidence":0.99902344,"speaker":"A"},{"text":"you","start":1927950,"end":1928110,"confidence":0.99853516,"speaker":"A"},{"text":"can","start":1928110,"end":1928270,"confidence":0.7426758,"speaker":"A"},{"text":"see","start":1928270,"end":1928430,"confidence":0.9995117,"speaker":"A"},{"text":"this.","start":1928430,"end":1928710,"confidence":0.9951172,"speaker":"A"}]},{"text":"I'm going to actually move over to my documentation here at this point. So how are we doing on time? We good?","start":1929110,"end":1940070,"confidence":0.99886066,"words":[{"text":"I'm","start":1929110,"end":1929430,"confidence":0.99886066,"speaker":"A"},{"text":"going","start":1929430,"end":1929550,"confidence":0.71240234,"speaker":"A"},{"text":"to","start":1929550,"end":1929710,"confidence":0.99902344,"speaker":"A"},{"text":"actually","start":1929710,"end":1929910,"confidence":0.9975586,"speaker":"A"},{"text":"move","start":1929910,"end":1930150,"confidence":0.9995117,"speaker":"A"},{"text":"over","start":1930150,"end":1930430,"confidence":0.9995117,"speaker":"A"},{"text":"to","start":1930430,"end":1930790,"confidence":0.99853516,"speaker":"A"},{"text":"my","start":1932470,"end":1932870,"confidence":0.99902344,"speaker":"A"},{"text":"documentation","start":1932950,"end":1933910,"confidence":0.99990237,"speaker":"A"},{"text":"here","start":1933910,"end":1934310,"confidence":0.99609375,"speaker":"A"},{"text":"at","start":1935270,"end":1935550,"confidence":0.9951172,"speaker":"A"},{"text":"this","start":1935550,"end":1935710,"confidence":1,"speaker":"A"},{"text":"point.","start":1935710,"end":1935990,"confidence":0.9995117,"speaker":"A"},{"text":"So","start":1936150,"end":1936550,"confidence":0.9145508,"speaker":"A"},{"text":"how","start":1938310,"end":1938590,"confidence":0.99853516,"speaker":"A"},{"text":"are","start":1938590,"end":1938710,"confidence":0.9394531,"speaker":"A"},{"text":"we","start":1938710,"end":1938830,"confidence":0.42895508,"speaker":"A"},{"text":"doing","start":1938830,"end":1938990,"confidence":0.9980469,"speaker":"A"},{"text":"on","start":1938990,"end":1939190,"confidence":0.99853516,"speaker":"A"},{"text":"time?","start":1939190,"end":1939510,"confidence":0.9995117,"speaker":"A"},{"text":"We","start":1939510,"end":1939790,"confidence":0.7001953,"speaker":"A"},{"text":"good?","start":1939790,"end":1940070,"confidence":0.98876953,"speaker":"A"}]},{"text":"Yeah, I think, I think we're doing good. Okay, cool. Any, do you want to ask questions? I don't have anything right now. Same nothing right now.","start":1942550,"end":1955040,"confidence":0.9842122,"words":[{"text":"Yeah,","start":1942550,"end":1942870,"confidence":0.9842122,"speaker":"B"},{"text":"I","start":1942870,"end":1942990,"confidence":0.59228516,"speaker":"B"},{"text":"think,","start":1942990,"end":1943190,"confidence":0.9770508,"speaker":"B"},{"text":"I","start":1943190,"end":1943350,"confidence":0.96240234,"speaker":"B"},{"text":"think","start":1943350,"end":1943470,"confidence":0.9975586,"speaker":"B"},{"text":"we're","start":1943470,"end":1943670,"confidence":0.99902344,"speaker":"B"},{"text":"doing","start":1943670,"end":1943790,"confidence":0.9980469,"speaker":"B"},{"text":"good.","start":1943790,"end":1944070,"confidence":0.9951172,"speaker":"B"},{"text":"Okay,","start":1944870,"end":1945310,"confidence":0.94189453,"speaker":"A"},{"text":"cool.","start":1945310,"end":1945590,"confidence":0.99780273,"speaker":"A"},{"text":"Any,","start":1945590,"end":1945910,"confidence":0.90234375,"speaker":"A"},{"text":"do","start":1946560,"end":1946640,"confidence":0.70996094,"speaker":"A"},{"text":"you","start":1946640,"end":1946760,"confidence":0.9946289,"speaker":"A"},{"text":"want","start":1946760,"end":1946880,"confidence":0.9321289,"speaker":"A"},{"text":"to","start":1946880,"end":1946960,"confidence":0.9980469,"speaker":"A"},{"text":"ask","start":1946960,"end":1947120,"confidence":0.9995117,"speaker":"A"},{"text":"questions?","start":1947120,"end":1947680,"confidence":0.99975586,"speaker":"A"},{"text":"I","start":1949680,"end":1949960,"confidence":0.9975586,"speaker":"B"},{"text":"don't","start":1949960,"end":1950240,"confidence":0.9991862,"speaker":"B"},{"text":"have","start":1950240,"end":1950480,"confidence":0.9995117,"speaker":"B"},{"text":"anything","start":1950480,"end":1950960,"confidence":0.99975586,"speaker":"B"},{"text":"right","start":1951440,"end":1951800,"confidence":0.99902344,"speaker":"B"},{"text":"now.","start":1951800,"end":1952160,"confidence":0.99853516,"speaker":"B"},{"text":"Same","start":1953760,"end":1954160,"confidence":0.98291016,"speaker":"C"},{"text":"nothing","start":1954240,"end":1954600,"confidence":0.99975586,"speaker":"C"},{"text":"right","start":1954600,"end":1954800,"confidence":0.9995117,"speaker":"C"},{"text":"now.","start":1954800,"end":1955040,"confidence":0.9995117,"speaker":"C"}]},{"text":"But this seems applicable to things I'll be doing coming up. Okay, cool.","start":1955040,"end":1960480,"confidence":0.9980469,"words":[{"text":"But","start":1955040,"end":1955240,"confidence":0.9980469,"speaker":"C"},{"text":"this","start":1955240,"end":1955440,"confidence":0.99853516,"speaker":"C"},{"text":"seems","start":1955440,"end":1955880,"confidence":0.99975586,"speaker":"C"},{"text":"applicable","start":1955880,"end":1956560,"confidence":0.99975586,"speaker":"C"},{"text":"to","start":1956560,"end":1956960,"confidence":0.9995117,"speaker":"C"},{"text":"things","start":1957280,"end":1957600,"confidence":1,"speaker":"C"},{"text":"I'll","start":1957600,"end":1957880,"confidence":0.98779297,"speaker":"C"},{"text":"be","start":1957880,"end":1958000,"confidence":0.9995117,"speaker":"C"},{"text":"doing","start":1958000,"end":1958200,"confidence":0.9995117,"speaker":"C"},{"text":"coming","start":1958200,"end":1958480,"confidence":0.99853516,"speaker":"C"},{"text":"up.","start":1958480,"end":1958800,"confidence":0.99609375,"speaker":"C"},{"text":"Okay,","start":1959360,"end":1960000,"confidence":0.88964844,"speaker":"A"},{"text":"cool.","start":1960000,"end":1960480,"confidence":0.99902344,"speaker":"A"}]},{"text":"So we have set up in the open. So we have an open API YAML file that you can pull up in Miskit, which is basically every like the documentation converted to YAML. And so what we do is you can set up in the YAML the field value requests and they have an enum type essentially for, for open API. So and then, so this has, you know, it could be one of either any of these types of. And then there's an enum in case you have a list.","start":1963200,"end":2003170,"confidence":0.8515625,"words":[{"text":"So","start":1963200,"end":1963600,"confidence":0.8515625,"speaker":"A"},{"text":"we","start":1964480,"end":1964760,"confidence":0.9838867,"speaker":"A"},{"text":"have","start":1964760,"end":1964960,"confidence":0.59765625,"speaker":"A"},{"text":"set","start":1964960,"end":1965200,"confidence":0.99902344,"speaker":"A"},{"text":"up","start":1965200,"end":1965520,"confidence":0.9716797,"speaker":"A"},{"text":"in","start":1965920,"end":1966280,"confidence":0.85595703,"speaker":"A"},{"text":"the","start":1966280,"end":1966640,"confidence":0.98291016,"speaker":"A"},{"text":"open.","start":1966800,"end":1967200,"confidence":0.9916992,"speaker":"A"},{"text":"So","start":1967200,"end":1967440,"confidence":0.93896484,"speaker":"A"},{"text":"we","start":1967440,"end":1967520,"confidence":0.9980469,"speaker":"A"},{"text":"have","start":1967520,"end":1967640,"confidence":0.99902344,"speaker":"A"},{"text":"an","start":1967640,"end":1967760,"confidence":0.9116211,"speaker":"A"},{"text":"open","start":1967760,"end":1967960,"confidence":0.99853516,"speaker":"A"},{"text":"API","start":1967960,"end":1968480,"confidence":0.9958496,"speaker":"A"},{"text":"YAML","start":1968480,"end":1968920,"confidence":0.9547526,"speaker":"A"},{"text":"file","start":1968920,"end":1969360,"confidence":0.99731445,"speaker":"A"},{"text":"that","start":1969760,"end":1970040,"confidence":0.9980469,"speaker":"A"},{"text":"you","start":1970040,"end":1970240,"confidence":0.99902344,"speaker":"A"},{"text":"can","start":1970240,"end":1970400,"confidence":0.99853516,"speaker":"A"},{"text":"pull","start":1970400,"end":1970560,"confidence":0.99975586,"speaker":"A"},{"text":"up","start":1970560,"end":1970680,"confidence":0.9995117,"speaker":"A"},{"text":"in","start":1970680,"end":1970880,"confidence":0.9970703,"speaker":"A"},{"text":"Miskit,","start":1970880,"end":1971520,"confidence":0.98657227,"speaker":"A"},{"text":"which","start":1972250,"end":1972370,"confidence":0.9975586,"speaker":"A"},{"text":"is","start":1972370,"end":1972650,"confidence":0.99902344,"speaker":"A"},{"text":"basically","start":1972730,"end":1973370,"confidence":0.99975586,"speaker":"A"},{"text":"every","start":1973370,"end":1973770,"confidence":0.99365234,"speaker":"A"},{"text":"like","start":1973770,"end":1974170,"confidence":0.98828125,"speaker":"A"},{"text":"the","start":1975050,"end":1975370,"confidence":0.99902344,"speaker":"A"},{"text":"documentation","start":1975370,"end":1976170,"confidence":0.99912107,"speaker":"A"},{"text":"converted","start":1976330,"end":1977010,"confidence":0.9996745,"speaker":"A"},{"text":"to","start":1977010,"end":1977210,"confidence":0.9975586,"speaker":"A"},{"text":"YAML.","start":1977210,"end":1977850,"confidence":0.71435547,"speaker":"A"},{"text":"And","start":1978410,"end":1978770,"confidence":0.99072266,"speaker":"A"},{"text":"so","start":1978770,"end":1978970,"confidence":1,"speaker":"A"},{"text":"what","start":1978970,"end":1979090,"confidence":0.9995117,"speaker":"A"},{"text":"we","start":1979090,"end":1979290,"confidence":1,"speaker":"A"},{"text":"do","start":1979290,"end":1979570,"confidence":0.9980469,"speaker":"A"},{"text":"is","start":1979570,"end":1979930,"confidence":0.6928711,"speaker":"A"},{"text":"you","start":1980090,"end":1980410,"confidence":1,"speaker":"A"},{"text":"can","start":1980410,"end":1980690,"confidence":1,"speaker":"A"},{"text":"set","start":1980690,"end":1980930,"confidence":0.9995117,"speaker":"A"},{"text":"up","start":1980930,"end":1981210,"confidence":0.9975586,"speaker":"A"},{"text":"in","start":1982490,"end":1982770,"confidence":0.98095703,"speaker":"A"},{"text":"the","start":1982770,"end":1982930,"confidence":0.9951172,"speaker":"A"},{"text":"YAML","start":1982930,"end":1983250,"confidence":0.8038737,"speaker":"A"},{"text":"the","start":1983250,"end":1983410,"confidence":0.97753906,"speaker":"A"},{"text":"field","start":1983410,"end":1983690,"confidence":0.9980469,"speaker":"A"},{"text":"value","start":1983770,"end":1984130,"confidence":1,"speaker":"A"},{"text":"requests","start":1984130,"end":1984690,"confidence":0.8439128,"speaker":"A"},{"text":"and","start":1984690,"end":1984810,"confidence":0.9970703,"speaker":"A"},{"text":"they","start":1984810,"end":1984930,"confidence":1,"speaker":"A"},{"text":"have","start":1984930,"end":1985090,"confidence":1,"speaker":"A"},{"text":"an","start":1985090,"end":1985290,"confidence":0.9633789,"speaker":"A"},{"text":"enum","start":1985290,"end":1985770,"confidence":0.8808594,"speaker":"A"},{"text":"type","start":1985770,"end":1986090,"confidence":0.8652344,"speaker":"A"},{"text":"essentially","start":1986090,"end":1986650,"confidence":0.94311523,"speaker":"A"},{"text":"for,","start":1987930,"end":1988330,"confidence":0.96875,"speaker":"A"},{"text":"for","start":1992090,"end":1992450,"confidence":0.9995117,"speaker":"A"},{"text":"open","start":1992450,"end":1992810,"confidence":0.9995117,"speaker":"A"},{"text":"API.","start":1992970,"end":1993610,"confidence":0.9975586,"speaker":"A"},{"text":"So","start":1993690,"end":1994090,"confidence":0.98583984,"speaker":"A"},{"text":"and","start":1994970,"end":1995250,"confidence":0.9350586,"speaker":"A"},{"text":"then,","start":1995250,"end":1995490,"confidence":0.39233398,"speaker":"A"},{"text":"so","start":1995490,"end":1995770,"confidence":0.9975586,"speaker":"A"},{"text":"this","start":1995770,"end":1996010,"confidence":0.99902344,"speaker":"A"},{"text":"has,","start":1996010,"end":1996330,"confidence":0.99853516,"speaker":"A"},{"text":"you","start":1996330,"end":1996570,"confidence":0.6645508,"speaker":"A"},{"text":"know,","start":1996570,"end":1996690,"confidence":0.97998047,"speaker":"A"},{"text":"it","start":1996690,"end":1996810,"confidence":0.9975586,"speaker":"A"},{"text":"could","start":1996810,"end":1996930,"confidence":0.9838867,"speaker":"A"},{"text":"be","start":1996930,"end":1997090,"confidence":1,"speaker":"A"},{"text":"one","start":1997090,"end":1997210,"confidence":0.99853516,"speaker":"A"},{"text":"of","start":1997210,"end":1997410,"confidence":0.99902344,"speaker":"A"},{"text":"either","start":1997410,"end":1997770,"confidence":0.9968262,"speaker":"A"},{"text":"any","start":1997770,"end":1998010,"confidence":0.9995117,"speaker":"A"},{"text":"of","start":1998010,"end":1998170,"confidence":1,"speaker":"A"},{"text":"these","start":1998170,"end":1998370,"confidence":0.99902344,"speaker":"A"},{"text":"types","start":1998370,"end":1998810,"confidence":0.9453125,"speaker":"A"},{"text":"of.","start":1998860,"end":1999020,"confidence":0.5004883,"speaker":"A"},{"text":"And","start":2000050,"end":2000210,"confidence":0.97216797,"speaker":"A"},{"text":"then","start":2000210,"end":2000530,"confidence":0.9995117,"speaker":"A"},{"text":"there's","start":2000850,"end":2001210,"confidence":0.99560547,"speaker":"A"},{"text":"an","start":2001210,"end":2001370,"confidence":0.76220703,"speaker":"A"},{"text":"enum","start":2001370,"end":2001850,"confidence":0.92211914,"speaker":"A"},{"text":"in","start":2001850,"end":2002090,"confidence":0.9995117,"speaker":"A"},{"text":"case","start":2002090,"end":2002290,"confidence":1,"speaker":"A"},{"text":"you","start":2002290,"end":2002530,"confidence":0.9995117,"speaker":"A"},{"text":"have","start":2002530,"end":2002730,"confidence":1,"speaker":"A"},{"text":"a","start":2002730,"end":2002890,"confidence":0.99902344,"speaker":"A"},{"text":"list.","start":2002890,"end":2003170,"confidence":0.9995117,"speaker":"A"}]},{"text":"So if you have a list value type there is an extra property called type and then that will tell you what type the. The list is. And it's homo homomorphic. It's all the same list type. You can't have lists of different types.","start":2004050,"end":2022210,"confidence":0.99560547,"words":[{"text":"So","start":2004050,"end":2004450,"confidence":0.99560547,"speaker":"A"},{"text":"if","start":2005250,"end":2005570,"confidence":0.9980469,"speaker":"A"},{"text":"you","start":2005570,"end":2005770,"confidence":1,"speaker":"A"},{"text":"have","start":2005770,"end":2005970,"confidence":1,"speaker":"A"},{"text":"a","start":2005970,"end":2006210,"confidence":0.99902344,"speaker":"A"},{"text":"list","start":2006210,"end":2006530,"confidence":0.9995117,"speaker":"A"},{"text":"value","start":2006850,"end":2007250,"confidence":0.9995117,"speaker":"A"},{"text":"type","start":2007330,"end":2007890,"confidence":0.99780273,"speaker":"A"},{"text":"there","start":2008530,"end":2008850,"confidence":1,"speaker":"A"},{"text":"is","start":2008850,"end":2009090,"confidence":1,"speaker":"A"},{"text":"an","start":2009090,"end":2009290,"confidence":0.9995117,"speaker":"A"},{"text":"extra","start":2009290,"end":2009690,"confidence":0.99975586,"speaker":"A"},{"text":"property","start":2009690,"end":2010290,"confidence":0.9995117,"speaker":"A"},{"text":"called","start":2010290,"end":2010690,"confidence":0.9995117,"speaker":"A"},{"text":"type","start":2011010,"end":2011450,"confidence":0.81103516,"speaker":"A"},{"text":"and","start":2011450,"end":2011690,"confidence":0.9951172,"speaker":"A"},{"text":"then","start":2011690,"end":2011850,"confidence":0.99365234,"speaker":"A"},{"text":"that","start":2011850,"end":2012010,"confidence":0.9995117,"speaker":"A"},{"text":"will","start":2012010,"end":2012210,"confidence":0.9995117,"speaker":"A"},{"text":"tell","start":2012210,"end":2012410,"confidence":1,"speaker":"A"},{"text":"you","start":2012410,"end":2012570,"confidence":1,"speaker":"A"},{"text":"what","start":2012570,"end":2012810,"confidence":0.59277344,"speaker":"A"},{"text":"type","start":2012810,"end":2013250,"confidence":0.8652344,"speaker":"A"},{"text":"the.","start":2013410,"end":2013810,"confidence":0.98876953,"speaker":"A"},{"text":"The","start":2014450,"end":2014730,"confidence":0.99853516,"speaker":"A"},{"text":"list","start":2014730,"end":2015010,"confidence":0.9995117,"speaker":"A"},{"text":"is.","start":2015010,"end":2015329,"confidence":0.9995117,"speaker":"A"},{"text":"And","start":2015329,"end":2015570,"confidence":0.99365234,"speaker":"A"},{"text":"it's","start":2015570,"end":2016050,"confidence":0.99397784,"speaker":"A"},{"text":"homo","start":2016530,"end":2017250,"confidence":0.8297526,"speaker":"A"},{"text":"homomorphic.","start":2017250,"end":2018450,"confidence":0.99763995,"speaker":"A"},{"text":"It's","start":2018690,"end":2019050,"confidence":0.9720052,"speaker":"A"},{"text":"all","start":2019050,"end":2019210,"confidence":0.99560547,"speaker":"A"},{"text":"the","start":2019210,"end":2019330,"confidence":0.9995117,"speaker":"A"},{"text":"same","start":2019330,"end":2019570,"confidence":0.99902344,"speaker":"A"},{"text":"list","start":2019890,"end":2020210,"confidence":0.97314453,"speaker":"A"},{"text":"type.","start":2020210,"end":2020490,"confidence":0.9848633,"speaker":"A"},{"text":"You","start":2020490,"end":2020610,"confidence":0.9995117,"speaker":"A"},{"text":"can't","start":2020610,"end":2020810,"confidence":0.98567706,"speaker":"A"},{"text":"have","start":2020810,"end":2021010,"confidence":1,"speaker":"A"},{"text":"lists","start":2021010,"end":2021330,"confidence":0.9987793,"speaker":"A"},{"text":"of","start":2021330,"end":2021450,"confidence":0.9995117,"speaker":"A"},{"text":"different","start":2021450,"end":2021690,"confidence":1,"speaker":"A"},{"text":"types.","start":2021690,"end":2022210,"confidence":0.92578125,"speaker":"A"}]},{"text":"And then we have here again field value. Sometimes the type is available, sometimes it's not. But basically we have all the different value types available to us in a CK value. And then this is. Then the Open API generator essentially builds this for me which is.","start":2024050,"end":2049150,"confidence":0.95751953,"words":[{"text":"And","start":2024050,"end":2024450,"confidence":0.95751953,"speaker":"A"},{"text":"then","start":2024610,"end":2025010,"confidence":0.9038086,"speaker":"A"},{"text":"we","start":2026030,"end":2026190,"confidence":0.9941406,"speaker":"A"},{"text":"have","start":2026190,"end":2026470,"confidence":0.9995117,"speaker":"A"},{"text":"here","start":2026470,"end":2026830,"confidence":0.99902344,"speaker":"A"},{"text":"again","start":2028830,"end":2029230,"confidence":0.99853516,"speaker":"A"},{"text":"field","start":2029230,"end":2029590,"confidence":0.9404297,"speaker":"A"},{"text":"value.","start":2029590,"end":2029950,"confidence":0.99902344,"speaker":"A"},{"text":"Sometimes","start":2031390,"end":2031910,"confidence":0.99886066,"speaker":"A"},{"text":"the","start":2031910,"end":2032070,"confidence":0.98876953,"speaker":"A"},{"text":"type","start":2032070,"end":2032310,"confidence":0.9086914,"speaker":"A"},{"text":"is","start":2032310,"end":2032470,"confidence":0.99853516,"speaker":"A"},{"text":"available,","start":2032470,"end":2032750,"confidence":0.9995117,"speaker":"A"},{"text":"sometimes","start":2032910,"end":2033430,"confidence":0.9996745,"speaker":"A"},{"text":"it's","start":2033430,"end":2033750,"confidence":0.99886066,"speaker":"A"},{"text":"not.","start":2033750,"end":2034030,"confidence":0.9995117,"speaker":"A"},{"text":"But","start":2034590,"end":2034910,"confidence":0.99658203,"speaker":"A"},{"text":"basically","start":2034910,"end":2035390,"confidence":0.99975586,"speaker":"A"},{"text":"we","start":2035390,"end":2035670,"confidence":0.9995117,"speaker":"A"},{"text":"have","start":2035670,"end":2035910,"confidence":0.9995117,"speaker":"A"},{"text":"all","start":2035910,"end":2036150,"confidence":1,"speaker":"A"},{"text":"the","start":2036150,"end":2036310,"confidence":0.9995117,"speaker":"A"},{"text":"different","start":2036310,"end":2036590,"confidence":0.9995117,"speaker":"A"},{"text":"value","start":2036750,"end":2037150,"confidence":0.99902344,"speaker":"A"},{"text":"types","start":2037230,"end":2037710,"confidence":0.99975586,"speaker":"A"},{"text":"available","start":2037710,"end":2038030,"confidence":0.99902344,"speaker":"A"},{"text":"to","start":2038190,"end":2038470,"confidence":1,"speaker":"A"},{"text":"us","start":2038470,"end":2038750,"confidence":1,"speaker":"A"},{"text":"in","start":2038830,"end":2039110,"confidence":0.97802734,"speaker":"A"},{"text":"a","start":2039110,"end":2039270,"confidence":0.96728516,"speaker":"A"},{"text":"CK","start":2039270,"end":2039630,"confidence":0.9001465,"speaker":"A"},{"text":"value.","start":2039630,"end":2039950,"confidence":0.9091797,"speaker":"A"},{"text":"And","start":2041950,"end":2042230,"confidence":0.9848633,"speaker":"A"},{"text":"then","start":2042230,"end":2042510,"confidence":0.99902344,"speaker":"A"},{"text":"this","start":2042990,"end":2043310,"confidence":0.99853516,"speaker":"A"},{"text":"is.","start":2043310,"end":2043550,"confidence":0.99902344,"speaker":"A"},{"text":"Then","start":2043550,"end":2043870,"confidence":0.9848633,"speaker":"A"},{"text":"the","start":2044110,"end":2044430,"confidence":0.98828125,"speaker":"A"},{"text":"Open","start":2044430,"end":2044750,"confidence":0.9946289,"speaker":"A"},{"text":"API","start":2045150,"end":2045670,"confidence":0.99780273,"speaker":"A"},{"text":"generator","start":2045670,"end":2046190,"confidence":0.97143555,"speaker":"A"},{"text":"essentially","start":2046190,"end":2046870,"confidence":0.99902344,"speaker":"A"},{"text":"builds","start":2046870,"end":2047310,"confidence":0.9782715,"speaker":"A"},{"text":"this","start":2047310,"end":2047470,"confidence":0.9926758,"speaker":"A"},{"text":"for","start":2047470,"end":2047670,"confidence":0.9838867,"speaker":"A"},{"text":"me","start":2047670,"end":2047950,"confidence":0.99853516,"speaker":"A"},{"text":"which","start":2048510,"end":2048830,"confidence":0.9980469,"speaker":"A"},{"text":"is.","start":2048830,"end":2049150,"confidence":0.9873047,"speaker":"A"}]},{"text":"Has an enum and a struck for field field value request and then it does all the decoding for me. Thankfully I didn't have to do any of it.","start":2049710,"end":2059169,"confidence":0.9980469,"words":[{"text":"Has","start":2049710,"end":2049990,"confidence":0.9980469,"speaker":"A"},{"text":"an","start":2049990,"end":2050150,"confidence":0.47924805,"speaker":"A"},{"text":"enum","start":2050150,"end":2050670,"confidence":0.7680664,"speaker":"A"},{"text":"and","start":2050830,"end":2051110,"confidence":0.9902344,"speaker":"A"},{"text":"a","start":2051110,"end":2051270,"confidence":0.9863281,"speaker":"A"},{"text":"struck","start":2051270,"end":2051510,"confidence":0.7644043,"speaker":"A"},{"text":"for","start":2051510,"end":2051670,"confidence":0.5751953,"speaker":"A"},{"text":"field","start":2051670,"end":2051950,"confidence":0.7363281,"speaker":"A"},{"text":"field","start":2052110,"end":2052510,"confidence":1,"speaker":"A"},{"text":"value","start":2052670,"end":2053070,"confidence":0.99902344,"speaker":"A"},{"text":"request","start":2053070,"end":2053630,"confidence":0.7783203,"speaker":"A"},{"text":"and","start":2055329,"end":2055449,"confidence":0.9321289,"speaker":"A"},{"text":"then","start":2055449,"end":2055609,"confidence":0.9946289,"speaker":"A"},{"text":"it","start":2055609,"end":2055769,"confidence":1,"speaker":"A"},{"text":"does","start":2055769,"end":2055929,"confidence":0.9995117,"speaker":"A"},{"text":"all","start":2055929,"end":2056089,"confidence":0.9941406,"speaker":"A"},{"text":"the","start":2056089,"end":2056249,"confidence":0.9946289,"speaker":"A"},{"text":"decoding","start":2056249,"end":2056769,"confidence":0.99886066,"speaker":"A"},{"text":"for","start":2056769,"end":2056969,"confidence":0.99902344,"speaker":"A"},{"text":"me.","start":2056969,"end":2057249,"confidence":1,"speaker":"A"},{"text":"Thankfully","start":2057249,"end":2057849,"confidence":0.99523926,"speaker":"A"},{"text":"I","start":2057849,"end":2058089,"confidence":0.99560547,"speaker":"A"},{"text":"didn't","start":2058089,"end":2058289,"confidence":0.95670575,"speaker":"A"},{"text":"have","start":2058289,"end":2058369,"confidence":0.99902344,"speaker":"A"},{"text":"to","start":2058369,"end":2058449,"confidence":0.9980469,"speaker":"A"},{"text":"do","start":2058449,"end":2058569,"confidence":0.91845703,"speaker":"A"},{"text":"any","start":2058569,"end":2058769,"confidence":1,"speaker":"A"},{"text":"of","start":2058769,"end":2058929,"confidence":0.9995117,"speaker":"A"},{"text":"it.","start":2058929,"end":2059169,"confidence":0.9975586,"speaker":"A"}]},{"text":"And then yeah, I just wanted to cover that piece where we show how we deal with these kind of like polymorphic types and how those work. The next thing I want to cover is error handling. So if you look at the documentation gives you. If you get an error we get something like this and then that will show you in the. In the table actually shows you what each error means.","start":2063089,"end":2093630,"confidence":0.97021484,"words":[{"text":"And","start":2063089,"end":2063369,"confidence":0.97021484,"speaker":"A"},{"text":"then","start":2063369,"end":2063649,"confidence":0.99658203,"speaker":"A"},{"text":"yeah,","start":2065409,"end":2065809,"confidence":0.94091797,"speaker":"A"},{"text":"I","start":2065809,"end":2066009,"confidence":0.99902344,"speaker":"A"},{"text":"just","start":2066009,"end":2066169,"confidence":0.99902344,"speaker":"A"},{"text":"wanted","start":2066169,"end":2066409,"confidence":0.99780273,"speaker":"A"},{"text":"to","start":2066409,"end":2066569,"confidence":0.99902344,"speaker":"A"},{"text":"cover","start":2066569,"end":2066769,"confidence":1,"speaker":"A"},{"text":"that","start":2066769,"end":2067009,"confidence":0.9995117,"speaker":"A"},{"text":"piece","start":2067009,"end":2067409,"confidence":0.9667969,"speaker":"A"},{"text":"where","start":2067569,"end":2067929,"confidence":0.9995117,"speaker":"A"},{"text":"we","start":2067929,"end":2068249,"confidence":0.9995117,"speaker":"A"},{"text":"show","start":2068249,"end":2068609,"confidence":0.99902344,"speaker":"A"},{"text":"how","start":2068929,"end":2069249,"confidence":0.9995117,"speaker":"A"},{"text":"we","start":2069249,"end":2069449,"confidence":1,"speaker":"A"},{"text":"deal","start":2069449,"end":2069609,"confidence":1,"speaker":"A"},{"text":"with","start":2069609,"end":2069888,"confidence":0.9995117,"speaker":"A"},{"text":"these","start":2069888,"end":2070209,"confidence":0.99072266,"speaker":"A"},{"text":"kind","start":2070209,"end":2070369,"confidence":0.98876953,"speaker":"A"},{"text":"of","start":2070369,"end":2070529,"confidence":0.5283203,"speaker":"A"},{"text":"like","start":2070529,"end":2070729,"confidence":0.984375,"speaker":"A"},{"text":"polymorphic","start":2070729,"end":2071969,"confidence":0.9777832,"speaker":"A"},{"text":"types","start":2071969,"end":2072529,"confidence":0.76416016,"speaker":"A"},{"text":"and","start":2073249,"end":2073529,"confidence":0.99658203,"speaker":"A"},{"text":"how","start":2073529,"end":2073729,"confidence":0.9995117,"speaker":"A"},{"text":"those","start":2073729,"end":2073969,"confidence":0.99902344,"speaker":"A"},{"text":"work.","start":2073969,"end":2074289,"confidence":0.99853516,"speaker":"A"},{"text":"The","start":2075329,"end":2075569,"confidence":0.9746094,"speaker":"A"},{"text":"next","start":2075569,"end":2075729,"confidence":0.9902344,"speaker":"A"},{"text":"thing","start":2075729,"end":2075889,"confidence":0.9692383,"speaker":"A"},{"text":"I","start":2075889,"end":2075969,"confidence":0.89208984,"speaker":"A"},{"text":"want","start":2075969,"end":2076089,"confidence":0.79052734,"speaker":"A"},{"text":"to","start":2076089,"end":2076209,"confidence":0.99902344,"speaker":"A"},{"text":"cover","start":2076209,"end":2076409,"confidence":0.9995117,"speaker":"A"},{"text":"is","start":2076409,"end":2076689,"confidence":0.99853516,"speaker":"A"},{"text":"error","start":2076689,"end":2077009,"confidence":0.914917,"speaker":"A"},{"text":"handling.","start":2077009,"end":2077489,"confidence":0.99902344,"speaker":"A"},{"text":"So","start":2079249,"end":2079529,"confidence":0.99121094,"speaker":"A"},{"text":"if","start":2079529,"end":2079729,"confidence":0.6791992,"speaker":"A"},{"text":"you","start":2079729,"end":2079929,"confidence":1,"speaker":"A"},{"text":"look","start":2079929,"end":2080049,"confidence":1,"speaker":"A"},{"text":"at","start":2080049,"end":2080169,"confidence":1,"speaker":"A"},{"text":"the","start":2080169,"end":2080289,"confidence":1,"speaker":"A"},{"text":"documentation","start":2080289,"end":2081009,"confidence":0.9964844,"speaker":"A"},{"text":"gives","start":2081569,"end":2081969,"confidence":0.9904785,"speaker":"A"},{"text":"you.","start":2081969,"end":2082209,"confidence":0.99658203,"speaker":"A"},{"text":"If","start":2083390,"end":2083510,"confidence":0.98876953,"speaker":"A"},{"text":"you","start":2083510,"end":2083630,"confidence":0.9975586,"speaker":"A"},{"text":"get","start":2083630,"end":2083750,"confidence":0.97509766,"speaker":"A"},{"text":"an","start":2083750,"end":2083910,"confidence":0.9604492,"speaker":"A"},{"text":"error","start":2083910,"end":2084270,"confidence":0.8522949,"speaker":"A"},{"text":"we","start":2085150,"end":2085430,"confidence":0.99121094,"speaker":"A"},{"text":"get","start":2085430,"end":2085630,"confidence":0.71777344,"speaker":"A"},{"text":"something","start":2085630,"end":2085870,"confidence":0.9995117,"speaker":"A"},{"text":"like","start":2085870,"end":2086070,"confidence":0.9995117,"speaker":"A"},{"text":"this","start":2086070,"end":2086350,"confidence":0.99902344,"speaker":"A"},{"text":"and","start":2088030,"end":2088350,"confidence":0.9238281,"speaker":"A"},{"text":"then","start":2088350,"end":2088630,"confidence":0.9921875,"speaker":"A"},{"text":"that","start":2088630,"end":2088910,"confidence":0.90283203,"speaker":"A"},{"text":"will","start":2088910,"end":2089150,"confidence":0.7714844,"speaker":"A"},{"text":"show","start":2089150,"end":2089350,"confidence":0.9995117,"speaker":"A"},{"text":"you","start":2089350,"end":2089630,"confidence":0.99658203,"speaker":"A"},{"text":"in","start":2089870,"end":2090150,"confidence":0.7524414,"speaker":"A"},{"text":"the.","start":2090150,"end":2090350,"confidence":0.80615234,"speaker":"A"},{"text":"In","start":2090350,"end":2090590,"confidence":0.98876953,"speaker":"A"},{"text":"the","start":2090590,"end":2090750,"confidence":0.9995117,"speaker":"A"},{"text":"table","start":2090750,"end":2091070,"confidence":0.9995117,"speaker":"A"},{"text":"actually","start":2091070,"end":2091390,"confidence":0.99853516,"speaker":"A"},{"text":"shows","start":2091390,"end":2091710,"confidence":0.99975586,"speaker":"A"},{"text":"you","start":2091710,"end":2091830,"confidence":0.9995117,"speaker":"A"},{"text":"what","start":2091830,"end":2092030,"confidence":0.9995117,"speaker":"A"},{"text":"each","start":2092030,"end":2092350,"confidence":0.9995117,"speaker":"A"},{"text":"error","start":2092830,"end":2093270,"confidence":0.87854004,"speaker":"A"},{"text":"means.","start":2093270,"end":2093630,"confidence":0.99853516,"speaker":"A"}]},{"text":"So again we do like an enum in YAML. It's basically a string and then we have everything else be a string. And then the open API generator will automatically generate this which gives us the server error code and the error response. It'll also do all this stuff here, which is really nice.","start":2094830,"end":2115500,"confidence":0.9707031,"words":[{"text":"So","start":2094830,"end":2095230,"confidence":0.9707031,"speaker":"A"},{"text":"again","start":2095230,"end":2095630,"confidence":0.9995117,"speaker":"A"},{"text":"we","start":2095710,"end":2095990,"confidence":1,"speaker":"A"},{"text":"do","start":2095990,"end":2096150,"confidence":0.9980469,"speaker":"A"},{"text":"like","start":2096150,"end":2096270,"confidence":0.9892578,"speaker":"A"},{"text":"an","start":2096270,"end":2096430,"confidence":0.9868164,"speaker":"A"},{"text":"enum","start":2096430,"end":2096990,"confidence":0.9489746,"speaker":"A"},{"text":"in","start":2097150,"end":2097470,"confidence":0.54541016,"speaker":"A"},{"text":"YAML.","start":2097470,"end":2098110,"confidence":0.94954425,"speaker":"A"},{"text":"It's","start":2098830,"end":2099190,"confidence":0.99853516,"speaker":"A"},{"text":"basically","start":2099190,"end":2099550,"confidence":0.99975586,"speaker":"A"},{"text":"a","start":2099550,"end":2099750,"confidence":0.9970703,"speaker":"A"},{"text":"string","start":2099750,"end":2100110,"confidence":0.99975586,"speaker":"A"},{"text":"and","start":2100110,"end":2100310,"confidence":0.99658203,"speaker":"A"},{"text":"then","start":2100310,"end":2100430,"confidence":0.9746094,"speaker":"A"},{"text":"we","start":2100430,"end":2100550,"confidence":0.9995117,"speaker":"A"},{"text":"have","start":2100550,"end":2100710,"confidence":0.9995117,"speaker":"A"},{"text":"everything","start":2100710,"end":2100910,"confidence":0.9995117,"speaker":"A"},{"text":"else","start":2100910,"end":2101190,"confidence":0.99975586,"speaker":"A"},{"text":"be","start":2101190,"end":2101350,"confidence":0.98046875,"speaker":"A"},{"text":"a","start":2101350,"end":2101510,"confidence":0.99853516,"speaker":"A"},{"text":"string.","start":2101510,"end":2101950,"confidence":0.99902344,"speaker":"A"},{"text":"And","start":2102590,"end":2102870,"confidence":0.96240234,"speaker":"A"},{"text":"then","start":2102870,"end":2103150,"confidence":0.99902344,"speaker":"A"},{"text":"the","start":2103310,"end":2103590,"confidence":0.9946289,"speaker":"A"},{"text":"open","start":2103590,"end":2103790,"confidence":0.9946289,"speaker":"A"},{"text":"API","start":2103790,"end":2104270,"confidence":0.95581055,"speaker":"A"},{"text":"generator","start":2104270,"end":2104790,"confidence":0.998291,"speaker":"A"},{"text":"will","start":2104790,"end":2105030,"confidence":0.9975586,"speaker":"A"},{"text":"automatically","start":2105030,"end":2105590,"confidence":0.8905029,"speaker":"A"},{"text":"generate","start":2105590,"end":2106110,"confidence":1,"speaker":"A"},{"text":"this","start":2106110,"end":2106430,"confidence":0.9970703,"speaker":"A"},{"text":"which","start":2107710,"end":2108110,"confidence":0.9975586,"speaker":"A"},{"text":"gives","start":2108110,"end":2108510,"confidence":0.9970703,"speaker":"A"},{"text":"us","start":2108510,"end":2108630,"confidence":0.99609375,"speaker":"A"},{"text":"the","start":2108630,"end":2108910,"confidence":0.53759766,"speaker":"A"},{"text":"server","start":2109500,"end":2109860,"confidence":0.9980469,"speaker":"A"},{"text":"error","start":2109860,"end":2110140,"confidence":0.986084,"speaker":"A"},{"text":"code","start":2110140,"end":2110500,"confidence":0.9977214,"speaker":"A"},{"text":"and","start":2110500,"end":2110740,"confidence":0.9145508,"speaker":"A"},{"text":"the","start":2110740,"end":2110980,"confidence":0.95751953,"speaker":"A"},{"text":"error","start":2110980,"end":2111220,"confidence":0.9855957,"speaker":"A"},{"text":"response.","start":2111220,"end":2111820,"confidence":0.89868164,"speaker":"A"},{"text":"It'll","start":2112380,"end":2112820,"confidence":0.9863281,"speaker":"A"},{"text":"also","start":2112820,"end":2113060,"confidence":1,"speaker":"A"},{"text":"do","start":2113060,"end":2113300,"confidence":1,"speaker":"A"},{"text":"all","start":2113300,"end":2113460,"confidence":1,"speaker":"A"},{"text":"this","start":2113460,"end":2113660,"confidence":0.61621094,"speaker":"A"},{"text":"stuff","start":2113660,"end":2113980,"confidence":1,"speaker":"A"},{"text":"here,","start":2113980,"end":2114260,"confidence":1,"speaker":"A"},{"text":"which","start":2114260,"end":2114580,"confidence":0.9399414,"speaker":"A"},{"text":"is","start":2114580,"end":2114820,"confidence":0.99658203,"speaker":"A"},{"text":"really","start":2114820,"end":2115060,"confidence":0.74316406,"speaker":"A"},{"text":"nice.","start":2115060,"end":2115500,"confidence":1,"speaker":"A"}]},{"text":"And then we've then in our. We've abstracted a lot of this in miskit. So that way we also have now a cloud cloud error type which gives us a lot more info regarding that.","start":2117980,"end":2131820,"confidence":0.9970703,"words":[{"text":"And","start":2117980,"end":2118260,"confidence":0.9970703,"speaker":"A"},{"text":"then","start":2118260,"end":2118540,"confidence":0.9995117,"speaker":"A"},{"text":"we've","start":2118620,"end":2119180,"confidence":0.9142253,"speaker":"A"},{"text":"then","start":2119180,"end":2119500,"confidence":0.953125,"speaker":"A"},{"text":"in","start":2119500,"end":2119700,"confidence":0.984375,"speaker":"A"},{"text":"our.","start":2119700,"end":2119980,"confidence":0.9980469,"speaker":"A"},{"text":"We've","start":2120140,"end":2120500,"confidence":0.9944661,"speaker":"A"},{"text":"abstracted","start":2120500,"end":2121220,"confidence":0.9979248,"speaker":"A"},{"text":"a","start":2121220,"end":2121340,"confidence":0.9995117,"speaker":"A"},{"text":"lot","start":2121340,"end":2121460,"confidence":1,"speaker":"A"},{"text":"of","start":2121460,"end":2121580,"confidence":1,"speaker":"A"},{"text":"this","start":2121580,"end":2121740,"confidence":0.99658203,"speaker":"A"},{"text":"in","start":2121740,"end":2121940,"confidence":0.72802734,"speaker":"A"},{"text":"miskit.","start":2121940,"end":2122620,"confidence":0.83813477,"speaker":"A"},{"text":"So","start":2122940,"end":2123180,"confidence":1,"speaker":"A"},{"text":"that","start":2123180,"end":2123340,"confidence":1,"speaker":"A"},{"text":"way","start":2123340,"end":2123660,"confidence":0.99902344,"speaker":"A"},{"text":"we","start":2123980,"end":2124260,"confidence":1,"speaker":"A"},{"text":"also","start":2124260,"end":2124460,"confidence":0.9995117,"speaker":"A"},{"text":"have","start":2124460,"end":2124740,"confidence":1,"speaker":"A"},{"text":"now","start":2124740,"end":2125100,"confidence":0.99853516,"speaker":"A"},{"text":"a","start":2125580,"end":2125860,"confidence":0.99658203,"speaker":"A"},{"text":"cloud","start":2125860,"end":2126220,"confidence":0.9638672,"speaker":"A"},{"text":"cloud","start":2126540,"end":2127100,"confidence":0.9489746,"speaker":"A"},{"text":"error","start":2127100,"end":2127500,"confidence":0.94311523,"speaker":"A"},{"text":"type","start":2127500,"end":2127980,"confidence":0.99975586,"speaker":"A"},{"text":"which","start":2128540,"end":2128900,"confidence":1,"speaker":"A"},{"text":"gives","start":2128900,"end":2129220,"confidence":1,"speaker":"A"},{"text":"us","start":2129220,"end":2129380,"confidence":1,"speaker":"A"},{"text":"a","start":2129380,"end":2129500,"confidence":1,"speaker":"A"},{"text":"lot","start":2129500,"end":2129660,"confidence":1,"speaker":"A"},{"text":"more","start":2129660,"end":2129980,"confidence":0.9995117,"speaker":"A"},{"text":"info","start":2130060,"end":2130700,"confidence":0.99975586,"speaker":"A"},{"text":"regarding","start":2130860,"end":2131460,"confidence":0.87874347,"speaker":"A"},{"text":"that.","start":2131460,"end":2131820,"confidence":0.99853516,"speaker":"A"}]},{"text":"So that's how we handle errors. And everything I do in the abs, the more abstract higher up stuff is done using type throws like I have type throws and everything. So that's how I handle that. Let me check one last piece I wanted to cover.","start":2133900,"end":2152200,"confidence":0.9975586,"words":[{"text":"So","start":2133900,"end":2134220,"confidence":0.9975586,"speaker":"A"},{"text":"that's","start":2134220,"end":2134540,"confidence":0.9998372,"speaker":"A"},{"text":"how","start":2134540,"end":2134660,"confidence":1,"speaker":"A"},{"text":"we","start":2134660,"end":2134820,"confidence":1,"speaker":"A"},{"text":"handle","start":2134820,"end":2135180,"confidence":0.99975586,"speaker":"A"},{"text":"errors.","start":2135180,"end":2135740,"confidence":0.99912107,"speaker":"A"},{"text":"And","start":2135820,"end":2136140,"confidence":0.99658203,"speaker":"A"},{"text":"everything","start":2136140,"end":2136460,"confidence":0.99902344,"speaker":"A"},{"text":"I","start":2137240,"end":2137360,"confidence":0.9736328,"speaker":"A"},{"text":"do","start":2137360,"end":2137520,"confidence":0.9995117,"speaker":"A"},{"text":"in","start":2137520,"end":2137680,"confidence":0.90283203,"speaker":"A"},{"text":"the","start":2137680,"end":2137800,"confidence":0.92822266,"speaker":"A"},{"text":"abs,","start":2137800,"end":2138080,"confidence":0.4827881,"speaker":"A"},{"text":"the","start":2138080,"end":2138360,"confidence":0.9897461,"speaker":"A"},{"text":"more","start":2138360,"end":2138600,"confidence":0.99072266,"speaker":"A"},{"text":"abstract","start":2138600,"end":2138960,"confidence":0.8538411,"speaker":"A"},{"text":"higher","start":2138960,"end":2139280,"confidence":0.99365234,"speaker":"A"},{"text":"up","start":2139280,"end":2139560,"confidence":0.9970703,"speaker":"A"},{"text":"stuff","start":2139560,"end":2139960,"confidence":0.9713542,"speaker":"A"},{"text":"is","start":2140280,"end":2140680,"confidence":0.99902344,"speaker":"A"},{"text":"done","start":2140680,"end":2141080,"confidence":0.9995117,"speaker":"A"},{"text":"using","start":2141800,"end":2142200,"confidence":1,"speaker":"A"},{"text":"type","start":2142360,"end":2142840,"confidence":0.77783203,"speaker":"A"},{"text":"throws","start":2142840,"end":2143320,"confidence":0.9947917,"speaker":"A"},{"text":"like","start":2143320,"end":2143560,"confidence":0.9794922,"speaker":"A"},{"text":"I","start":2143560,"end":2143760,"confidence":0.99902344,"speaker":"A"},{"text":"have","start":2143760,"end":2143960,"confidence":0.9995117,"speaker":"A"},{"text":"type","start":2143960,"end":2144240,"confidence":0.7751465,"speaker":"A"},{"text":"throws","start":2144240,"end":2144560,"confidence":0.9274089,"speaker":"A"},{"text":"and","start":2144560,"end":2144680,"confidence":0.5439453,"speaker":"A"},{"text":"everything.","start":2144680,"end":2144920,"confidence":0.9995117,"speaker":"A"},{"text":"So","start":2145160,"end":2145560,"confidence":0.9941406,"speaker":"A"},{"text":"that's","start":2145960,"end":2146360,"confidence":0.9996745,"speaker":"A"},{"text":"how","start":2146360,"end":2146440,"confidence":1,"speaker":"A"},{"text":"I","start":2146440,"end":2146560,"confidence":0.9995117,"speaker":"A"},{"text":"handle","start":2146560,"end":2146960,"confidence":0.9951172,"speaker":"A"},{"text":"that.","start":2146960,"end":2147240,"confidence":0.9970703,"speaker":"A"},{"text":"Let","start":2148600,"end":2148880,"confidence":0.97753906,"speaker":"A"},{"text":"me","start":2148880,"end":2149040,"confidence":0.9995117,"speaker":"A"},{"text":"check","start":2149040,"end":2149400,"confidence":0.99780273,"speaker":"A"},{"text":"one","start":2150600,"end":2150920,"confidence":0.99560547,"speaker":"A"},{"text":"last","start":2150920,"end":2151160,"confidence":0.99853516,"speaker":"A"},{"text":"piece","start":2151160,"end":2151440,"confidence":1,"speaker":"A"},{"text":"I","start":2151440,"end":2151560,"confidence":0.99853516,"speaker":"A"},{"text":"wanted","start":2151560,"end":2151800,"confidence":0.99902344,"speaker":"A"},{"text":"to","start":2151800,"end":2151920,"confidence":0.99902344,"speaker":"A"},{"text":"cover.","start":2151920,"end":2152200,"confidence":0.9980469,"speaker":"A"}]},{"text":"The last piece I want to cover is really cool. And that is the authentication layer. So Open API provides what's called middleware and that allows you to, when you create a client or a server, you can plug that in and it will handle like let's say you need to make modifications with the request or response. When it comes in, you can intercept it and make whatever modifications you want to make. And in this case what we've done is I've created an authentication middleware which then sees if you have what's called a token manager and an authentic you have that and an authentication method.","start":2154920,"end":2197590,"confidence":0.3737793,"words":[{"text":"The","start":2154920,"end":2155200,"confidence":0.3737793,"speaker":"A"},{"text":"last","start":2155200,"end":2155360,"confidence":0.9980469,"speaker":"A"},{"text":"piece","start":2155360,"end":2155600,"confidence":0.9995117,"speaker":"A"},{"text":"I","start":2155600,"end":2155720,"confidence":0.97998047,"speaker":"A"},{"text":"want","start":2155720,"end":2155840,"confidence":0.9321289,"speaker":"A"},{"text":"to","start":2155840,"end":2155960,"confidence":0.9916992,"speaker":"A"},{"text":"cover","start":2155960,"end":2156160,"confidence":1,"speaker":"A"},{"text":"is","start":2156160,"end":2156520,"confidence":0.99902344,"speaker":"A"},{"text":"really","start":2156760,"end":2157120,"confidence":0.9995117,"speaker":"A"},{"text":"cool.","start":2157120,"end":2157440,"confidence":0.99975586,"speaker":"A"},{"text":"And","start":2157440,"end":2157680,"confidence":0.7548828,"speaker":"A"},{"text":"that","start":2157680,"end":2157920,"confidence":1,"speaker":"A"},{"text":"is","start":2157920,"end":2158200,"confidence":0.9995117,"speaker":"A"},{"text":"the","start":2158200,"end":2158520,"confidence":1,"speaker":"A"},{"text":"authentication","start":2158520,"end":2159280,"confidence":0.9998779,"speaker":"A"},{"text":"layer.","start":2159280,"end":2159800,"confidence":0.9975586,"speaker":"A"},{"text":"So","start":2160200,"end":2160480,"confidence":0.9770508,"speaker":"A"},{"text":"Open","start":2160480,"end":2160720,"confidence":0.9995117,"speaker":"A"},{"text":"API","start":2160720,"end":2161320,"confidence":0.9436035,"speaker":"A"},{"text":"provides","start":2161320,"end":2161920,"confidence":0.99975586,"speaker":"A"},{"text":"what's","start":2161920,"end":2162240,"confidence":0.99902344,"speaker":"A"},{"text":"called","start":2162240,"end":2162480,"confidence":1,"speaker":"A"},{"text":"middleware","start":2162480,"end":2163160,"confidence":0.9995117,"speaker":"A"},{"text":"and","start":2164440,"end":2164680,"confidence":0.9550781,"speaker":"A"},{"text":"that","start":2164760,"end":2165080,"confidence":0.99902344,"speaker":"A"},{"text":"allows","start":2165080,"end":2165440,"confidence":1,"speaker":"A"},{"text":"you","start":2165440,"end":2165640,"confidence":0.9995117,"speaker":"A"},{"text":"to,","start":2165640,"end":2165960,"confidence":0.99072266,"speaker":"A"},{"text":"when","start":2166200,"end":2166480,"confidence":0.99658203,"speaker":"A"},{"text":"you","start":2166480,"end":2166600,"confidence":0.9892578,"speaker":"A"},{"text":"create","start":2166600,"end":2166720,"confidence":0.9995117,"speaker":"A"},{"text":"a","start":2166720,"end":2166880,"confidence":0.99902344,"speaker":"A"},{"text":"client","start":2166880,"end":2167120,"confidence":0.99975586,"speaker":"A"},{"text":"or","start":2167120,"end":2167320,"confidence":0.9995117,"speaker":"A"},{"text":"a","start":2167320,"end":2167520,"confidence":0.9916992,"speaker":"A"},{"text":"server,","start":2167520,"end":2167840,"confidence":0.99975586,"speaker":"A"},{"text":"you","start":2167840,"end":2167960,"confidence":0.99902344,"speaker":"A"},{"text":"can","start":2167960,"end":2168080,"confidence":1,"speaker":"A"},{"text":"plug","start":2168080,"end":2168360,"confidence":0.99975586,"speaker":"A"},{"text":"that","start":2168360,"end":2168560,"confidence":0.9995117,"speaker":"A"},{"text":"in","start":2168560,"end":2168760,"confidence":0.9975586,"speaker":"A"},{"text":"and","start":2168760,"end":2168960,"confidence":0.9980469,"speaker":"A"},{"text":"it","start":2168960,"end":2169120,"confidence":0.99902344,"speaker":"A"},{"text":"will","start":2169120,"end":2169280,"confidence":0.99902344,"speaker":"A"},{"text":"handle","start":2169280,"end":2169800,"confidence":0.99902344,"speaker":"A"},{"text":"like","start":2169880,"end":2170240,"confidence":0.9291992,"speaker":"A"},{"text":"let's","start":2170240,"end":2170520,"confidence":0.99934894,"speaker":"A"},{"text":"say","start":2170520,"end":2170640,"confidence":1,"speaker":"A"},{"text":"you","start":2170640,"end":2170760,"confidence":1,"speaker":"A"},{"text":"need","start":2170760,"end":2170880,"confidence":1,"speaker":"A"},{"text":"to","start":2170880,"end":2171000,"confidence":1,"speaker":"A"},{"text":"make","start":2171000,"end":2171120,"confidence":1,"speaker":"A"},{"text":"modifications","start":2171120,"end":2171840,"confidence":0.9995117,"speaker":"A"},{"text":"with","start":2171840,"end":2172080,"confidence":0.99902344,"speaker":"A"},{"text":"the","start":2172080,"end":2172240,"confidence":0.9951172,"speaker":"A"},{"text":"request","start":2172240,"end":2172600,"confidence":0.9995117,"speaker":"A"},{"text":"or","start":2172600,"end":2172800,"confidence":0.98779297,"speaker":"A"},{"text":"response.","start":2172800,"end":2173400,"confidence":0.9970703,"speaker":"A"},{"text":"When","start":2173640,"end":2173920,"confidence":1,"speaker":"A"},{"text":"it","start":2173920,"end":2174080,"confidence":0.99902344,"speaker":"A"},{"text":"comes","start":2174080,"end":2174280,"confidence":1,"speaker":"A"},{"text":"in,","start":2174280,"end":2174600,"confidence":0.99658203,"speaker":"A"},{"text":"you","start":2174680,"end":2174960,"confidence":1,"speaker":"A"},{"text":"can","start":2174960,"end":2175120,"confidence":0.9995117,"speaker":"A"},{"text":"intercept","start":2175120,"end":2175520,"confidence":0.8586426,"speaker":"A"},{"text":"it","start":2175520,"end":2175760,"confidence":0.9995117,"speaker":"A"},{"text":"and","start":2175760,"end":2175880,"confidence":0.9995117,"speaker":"A"},{"text":"make","start":2175880,"end":2176040,"confidence":0.9995117,"speaker":"A"},{"text":"whatever","start":2176040,"end":2176360,"confidence":0.9995117,"speaker":"A"},{"text":"modifications","start":2176360,"end":2177040,"confidence":0.99886066,"speaker":"A"},{"text":"you","start":2177040,"end":2177280,"confidence":0.9995117,"speaker":"A"},{"text":"want","start":2177280,"end":2177440,"confidence":0.9277344,"speaker":"A"},{"text":"to","start":2177440,"end":2177560,"confidence":0.9980469,"speaker":"A"},{"text":"make.","start":2177560,"end":2177800,"confidence":0.9980469,"speaker":"A"},{"text":"And","start":2179239,"end":2179519,"confidence":0.9013672,"speaker":"A"},{"text":"in","start":2179519,"end":2179640,"confidence":1,"speaker":"A"},{"text":"this","start":2179640,"end":2179800,"confidence":1,"speaker":"A"},{"text":"case","start":2179800,"end":2180120,"confidence":1,"speaker":"A"},{"text":"what","start":2180840,"end":2181160,"confidence":0.9995117,"speaker":"A"},{"text":"we've","start":2181160,"end":2181440,"confidence":0.9941406,"speaker":"A"},{"text":"done","start":2181440,"end":2181720,"confidence":1,"speaker":"A"},{"text":"is","start":2181720,"end":2182120,"confidence":0.9970703,"speaker":"A"},{"text":"I've","start":2182520,"end":2182880,"confidence":0.9954427,"speaker":"A"},{"text":"created","start":2182880,"end":2183320,"confidence":0.99975586,"speaker":"A"},{"text":"an","start":2184520,"end":2184840,"confidence":0.9926758,"speaker":"A"},{"text":"authentication","start":2184840,"end":2185480,"confidence":1,"speaker":"A"},{"text":"middleware","start":2185480,"end":2186200,"confidence":0.9993164,"speaker":"A"},{"text":"which","start":2187480,"end":2187840,"confidence":0.99902344,"speaker":"A"},{"text":"then","start":2187840,"end":2188200,"confidence":0.99902344,"speaker":"A"},{"text":"sees","start":2188600,"end":2189080,"confidence":0.8354492,"speaker":"A"},{"text":"if","start":2189080,"end":2189280,"confidence":0.99853516,"speaker":"A"},{"text":"you","start":2189280,"end":2189480,"confidence":0.99365234,"speaker":"A"},{"text":"have","start":2189480,"end":2189800,"confidence":0.9946289,"speaker":"A"},{"text":"what's","start":2191430,"end":2191670,"confidence":0.9420573,"speaker":"A"},{"text":"called","start":2191670,"end":2191790,"confidence":1,"speaker":"A"},{"text":"a","start":2191790,"end":2191910,"confidence":0.9916992,"speaker":"A"},{"text":"token","start":2191910,"end":2192270,"confidence":0.9996745,"speaker":"A"},{"text":"manager","start":2192270,"end":2192870,"confidence":0.9995117,"speaker":"A"},{"text":"and","start":2193990,"end":2194390,"confidence":0.98828125,"speaker":"A"},{"text":"an","start":2194390,"end":2194750,"confidence":0.7910156,"speaker":"A"},{"text":"authentic","start":2194750,"end":2195310,"confidence":0.97542316,"speaker":"A"},{"text":"you","start":2195310,"end":2195470,"confidence":0.9970703,"speaker":"A"},{"text":"have","start":2195470,"end":2195630,"confidence":1,"speaker":"A"},{"text":"that","start":2195630,"end":2195870,"confidence":0.9995117,"speaker":"A"},{"text":"and","start":2195870,"end":2196190,"confidence":0.9975586,"speaker":"A"},{"text":"an","start":2196190,"end":2196430,"confidence":0.9980469,"speaker":"A"},{"text":"authentication","start":2196430,"end":2197070,"confidence":0.99938965,"speaker":"A"},{"text":"method.","start":2197070,"end":2197590,"confidence":0.9983724,"speaker":"A"}]},{"text":"And the way it works is you pick what type of authentication you want to use. If you already have like a pre existing web token or you already have, or you, you know, have your key ID and your private key already, or you just have the API token. We've created basically a middleware that uses that. So this is how it creates the headers for server to server. So it does all this for us.","start":2198070,"end":2224160,"confidence":0.9921875,"words":[{"text":"And","start":2198070,"end":2198430,"confidence":0.9921875,"speaker":"A"},{"text":"the","start":2198430,"end":2198670,"confidence":1,"speaker":"A"},{"text":"way","start":2198670,"end":2198790,"confidence":1,"speaker":"A"},{"text":"it","start":2198790,"end":2198910,"confidence":0.99902344,"speaker":"A"},{"text":"works","start":2198910,"end":2199350,"confidence":0.9995117,"speaker":"A"},{"text":"is","start":2199510,"end":2199910,"confidence":0.99902344,"speaker":"A"},{"text":"you","start":2199910,"end":2200230,"confidence":1,"speaker":"A"},{"text":"pick","start":2200230,"end":2200550,"confidence":0.99853516,"speaker":"A"},{"text":"what","start":2201190,"end":2201550,"confidence":0.99365234,"speaker":"A"},{"text":"type","start":2201550,"end":2201830,"confidence":0.99975586,"speaker":"A"},{"text":"of","start":2201830,"end":2201990,"confidence":0.9995117,"speaker":"A"},{"text":"authentication","start":2201990,"end":2202550,"confidence":0.9998779,"speaker":"A"},{"text":"you","start":2202550,"end":2202710,"confidence":0.99902344,"speaker":"A"},{"text":"want","start":2202710,"end":2202830,"confidence":0.9165039,"speaker":"A"},{"text":"to","start":2202830,"end":2202950,"confidence":0.99609375,"speaker":"A"},{"text":"use.","start":2202950,"end":2203070,"confidence":1,"speaker":"A"},{"text":"If","start":2203070,"end":2203230,"confidence":1,"speaker":"A"},{"text":"you","start":2203230,"end":2203350,"confidence":1,"speaker":"A"},{"text":"already","start":2203350,"end":2203510,"confidence":0.99853516,"speaker":"A"},{"text":"have","start":2203510,"end":2203670,"confidence":1,"speaker":"A"},{"text":"like","start":2203670,"end":2203790,"confidence":0.99560547,"speaker":"A"},{"text":"a","start":2203790,"end":2203910,"confidence":0.9995117,"speaker":"A"},{"text":"pre","start":2203910,"end":2204030,"confidence":1,"speaker":"A"},{"text":"existing","start":2204030,"end":2204430,"confidence":0.98551434,"speaker":"A"},{"text":"web","start":2204430,"end":2204670,"confidence":0.99975586,"speaker":"A"},{"text":"token","start":2204670,"end":2205190,"confidence":0.9552409,"speaker":"A"},{"text":"or","start":2205590,"end":2205950,"confidence":0.99853516,"speaker":"A"},{"text":"you","start":2205950,"end":2206190,"confidence":0.99853516,"speaker":"A"},{"text":"already","start":2206190,"end":2206470,"confidence":0.99853516,"speaker":"A"},{"text":"have,","start":2206470,"end":2206789,"confidence":0.92626953,"speaker":"A"},{"text":"or","start":2206789,"end":2207070,"confidence":0.95996094,"speaker":"A"},{"text":"you,","start":2207070,"end":2207350,"confidence":0.9916992,"speaker":"A"},{"text":"you","start":2207350,"end":2207550,"confidence":0.9770508,"speaker":"A"},{"text":"know,","start":2207550,"end":2207710,"confidence":0.9716797,"speaker":"A"},{"text":"have","start":2207710,"end":2207910,"confidence":0.6328125,"speaker":"A"},{"text":"your","start":2207910,"end":2208110,"confidence":0.99853516,"speaker":"A"},{"text":"key","start":2208110,"end":2208310,"confidence":0.99609375,"speaker":"A"},{"text":"ID","start":2208310,"end":2208590,"confidence":0.97753906,"speaker":"A"},{"text":"and","start":2208590,"end":2208830,"confidence":0.99902344,"speaker":"A"},{"text":"your","start":2208830,"end":2208990,"confidence":0.99902344,"speaker":"A"},{"text":"private","start":2208990,"end":2209230,"confidence":1,"speaker":"A"},{"text":"key","start":2209230,"end":2209510,"confidence":0.9995117,"speaker":"A"},{"text":"already,","start":2209510,"end":2209830,"confidence":0.99560547,"speaker":"A"},{"text":"or","start":2209910,"end":2210190,"confidence":0.9995117,"speaker":"A"},{"text":"you","start":2210190,"end":2210350,"confidence":0.9995117,"speaker":"A"},{"text":"just","start":2210350,"end":2210510,"confidence":1,"speaker":"A"},{"text":"have","start":2210510,"end":2210670,"confidence":0.9995117,"speaker":"A"},{"text":"the","start":2210670,"end":2210790,"confidence":0.98339844,"speaker":"A"},{"text":"API","start":2210790,"end":2211190,"confidence":0.9992676,"speaker":"A"},{"text":"token.","start":2211190,"end":2211750,"confidence":0.99934894,"speaker":"A"},{"text":"We've","start":2212390,"end":2212790,"confidence":0.9996745,"speaker":"A"},{"text":"created","start":2212790,"end":2213190,"confidence":0.9995117,"speaker":"A"},{"text":"basically","start":2213190,"end":2213590,"confidence":0.9995117,"speaker":"A"},{"text":"a","start":2213590,"end":2213750,"confidence":0.99609375,"speaker":"A"},{"text":"middleware","start":2213750,"end":2214270,"confidence":0.99716794,"speaker":"A"},{"text":"that","start":2214270,"end":2214470,"confidence":0.99902344,"speaker":"A"},{"text":"uses","start":2214470,"end":2214870,"confidence":0.9992676,"speaker":"A"},{"text":"that.","start":2214870,"end":2215190,"confidence":0.98339844,"speaker":"A"},{"text":"So","start":2216560,"end":2216800,"confidence":0.7055664,"speaker":"A"},{"text":"this","start":2218880,"end":2219120,"confidence":0.9995117,"speaker":"A"},{"text":"is","start":2219120,"end":2219280,"confidence":0.99902344,"speaker":"A"},{"text":"how","start":2219280,"end":2219560,"confidence":1,"speaker":"A"},{"text":"it","start":2219560,"end":2219840,"confidence":0.9995117,"speaker":"A"},{"text":"creates","start":2219840,"end":2220200,"confidence":0.99902344,"speaker":"A"},{"text":"the","start":2220200,"end":2220360,"confidence":0.9995117,"speaker":"A"},{"text":"headers","start":2220360,"end":2220800,"confidence":0.99902344,"speaker":"A"},{"text":"for","start":2221040,"end":2221360,"confidence":0.98583984,"speaker":"A"},{"text":"server","start":2221360,"end":2221720,"confidence":0.9992676,"speaker":"A"},{"text":"to","start":2221720,"end":2221920,"confidence":0.96972656,"speaker":"A"},{"text":"server.","start":2221920,"end":2222400,"confidence":0.9992676,"speaker":"A"},{"text":"So","start":2222800,"end":2223040,"confidence":0.8354492,"speaker":"A"},{"text":"it","start":2223040,"end":2223160,"confidence":0.98583984,"speaker":"A"},{"text":"does","start":2223160,"end":2223320,"confidence":1,"speaker":"A"},{"text":"all","start":2223320,"end":2223480,"confidence":1,"speaker":"A"},{"text":"this","start":2223480,"end":2223640,"confidence":0.9970703,"speaker":"A"},{"text":"for","start":2223640,"end":2223840,"confidence":0.9995117,"speaker":"A"},{"text":"us.","start":2223840,"end":2224160,"confidence":0.99072266,"speaker":"A"}]},{"text":"And then what I added, which I think is really nice, is called the adaptive token manager. And the idea with that is like let's say you're using a client and you have the web authentication token now and then this allows you to upgrade with that web authentication token to the private database and have access to that.","start":2225760,"end":2247730,"confidence":0.6791992,"words":[{"text":"And","start":2225760,"end":2226040,"confidence":0.6791992,"speaker":"A"},{"text":"then","start":2226040,"end":2226320,"confidence":0.9941406,"speaker":"A"},{"text":"what","start":2227520,"end":2227760,"confidence":0.9873047,"speaker":"A"},{"text":"I","start":2227760,"end":2227880,"confidence":0.9980469,"speaker":"A"},{"text":"added,","start":2227880,"end":2228160,"confidence":0.99658203,"speaker":"A"},{"text":"which","start":2228480,"end":2228760,"confidence":0.9970703,"speaker":"A"},{"text":"I","start":2228760,"end":2228920,"confidence":0.9995117,"speaker":"A"},{"text":"think","start":2228920,"end":2229040,"confidence":1,"speaker":"A"},{"text":"is","start":2229040,"end":2229160,"confidence":0.9975586,"speaker":"A"},{"text":"really","start":2229160,"end":2229320,"confidence":0.9995117,"speaker":"A"},{"text":"nice,","start":2229320,"end":2229600,"confidence":1,"speaker":"A"},{"text":"is","start":2229600,"end":2229800,"confidence":0.68310547,"speaker":"A"},{"text":"called","start":2229800,"end":2229960,"confidence":0.9995117,"speaker":"A"},{"text":"the","start":2229960,"end":2230120,"confidence":0.9975586,"speaker":"A"},{"text":"adaptive","start":2230120,"end":2230720,"confidence":0.9437256,"speaker":"A"},{"text":"token","start":2230720,"end":2231240,"confidence":0.84195966,"speaker":"A"},{"text":"manager.","start":2231240,"end":2231760,"confidence":0.9963379,"speaker":"A"},{"text":"And","start":2232240,"end":2232520,"confidence":0.6923828,"speaker":"A"},{"text":"the","start":2232520,"end":2232680,"confidence":0.9995117,"speaker":"A"},{"text":"idea","start":2232680,"end":2233000,"confidence":1,"speaker":"A"},{"text":"with","start":2233000,"end":2233160,"confidence":0.99609375,"speaker":"A"},{"text":"that","start":2233160,"end":2233360,"confidence":0.9995117,"speaker":"A"},{"text":"is","start":2233360,"end":2233600,"confidence":0.9975586,"speaker":"A"},{"text":"like","start":2233600,"end":2233880,"confidence":0.8354492,"speaker":"A"},{"text":"let's","start":2233880,"end":2234240,"confidence":0.9013672,"speaker":"A"},{"text":"say","start":2234240,"end":2234560,"confidence":0.9995117,"speaker":"A"},{"text":"you're","start":2236960,"end":2237360,"confidence":0.9977214,"speaker":"A"},{"text":"using","start":2237360,"end":2237520,"confidence":0.9995117,"speaker":"A"},{"text":"a","start":2237520,"end":2237720,"confidence":0.99902344,"speaker":"A"},{"text":"client","start":2237720,"end":2238160,"confidence":0.99975586,"speaker":"A"},{"text":"and","start":2238240,"end":2238560,"confidence":0.9926758,"speaker":"A"},{"text":"you","start":2238560,"end":2238880,"confidence":0.99902344,"speaker":"A"},{"text":"have","start":2238880,"end":2239280,"confidence":1,"speaker":"A"},{"text":"the","start":2239280,"end":2239560,"confidence":0.9995117,"speaker":"A"},{"text":"web","start":2239560,"end":2239800,"confidence":0.9995117,"speaker":"A"},{"text":"authentication","start":2239800,"end":2240480,"confidence":0.8408203,"speaker":"A"},{"text":"token","start":2240480,"end":2240920,"confidence":0.9995117,"speaker":"A"},{"text":"now","start":2240920,"end":2241200,"confidence":0.91308594,"speaker":"A"},{"text":"and","start":2241440,"end":2241720,"confidence":0.94628906,"speaker":"A"},{"text":"then","start":2241720,"end":2242000,"confidence":0.97216797,"speaker":"A"},{"text":"this","start":2242080,"end":2242360,"confidence":0.9975586,"speaker":"A"},{"text":"allows","start":2242360,"end":2242640,"confidence":1,"speaker":"A"},{"text":"you","start":2242640,"end":2242760,"confidence":0.9995117,"speaker":"A"},{"text":"to","start":2242760,"end":2242920,"confidence":0.9980469,"speaker":"A"},{"text":"upgrade","start":2242920,"end":2243440,"confidence":0.9767253,"speaker":"A"},{"text":"with","start":2243810,"end":2243970,"confidence":0.9770508,"speaker":"A"},{"text":"that","start":2243970,"end":2244170,"confidence":0.9995117,"speaker":"A"},{"text":"web","start":2244170,"end":2244410,"confidence":0.998291,"speaker":"A"},{"text":"authentication","start":2244410,"end":2245090,"confidence":0.99938965,"speaker":"A"},{"text":"token","start":2245090,"end":2245450,"confidence":0.9991862,"speaker":"A"},{"text":"to","start":2245450,"end":2245610,"confidence":0.99560547,"speaker":"A"},{"text":"the","start":2245610,"end":2245770,"confidence":1,"speaker":"A"},{"text":"private","start":2245770,"end":2245970,"confidence":1,"speaker":"A"},{"text":"database","start":2245970,"end":2246490,"confidence":0.9998372,"speaker":"A"},{"text":"and","start":2246490,"end":2246690,"confidence":0.99853516,"speaker":"A"},{"text":"have","start":2246690,"end":2246930,"confidence":0.99560547,"speaker":"A"},{"text":"access","start":2246930,"end":2247210,"confidence":1,"speaker":"A"},{"text":"to","start":2247210,"end":2247450,"confidence":0.9995117,"speaker":"A"},{"text":"that.","start":2247450,"end":2247730,"confidence":0.9995117,"speaker":"A"}]},{"text":"So and then all the, all the signing is done before you in miskit for the server to server because stuff that needs to be signed, etc. And it takes care of all that. All stuff that Claude was essentially able to decipher from the documentation.","start":2250530,"end":2270060,"confidence":0.97558594,"words":[{"text":"So","start":2250530,"end":2250850,"confidence":0.97558594,"speaker":"A"},{"text":"and","start":2250850,"end":2251050,"confidence":0.97558594,"speaker":"A"},{"text":"then","start":2251050,"end":2251210,"confidence":0.97753906,"speaker":"A"},{"text":"all","start":2251210,"end":2251490,"confidence":0.9658203,"speaker":"A"},{"text":"the,","start":2251490,"end":2251890,"confidence":0.9921875,"speaker":"A"},{"text":"all","start":2252690,"end":2252970,"confidence":0.9013672,"speaker":"A"},{"text":"the","start":2252970,"end":2253170,"confidence":0.99609375,"speaker":"A"},{"text":"signing","start":2253170,"end":2253610,"confidence":0.99658203,"speaker":"A"},{"text":"is","start":2253610,"end":2253770,"confidence":0.9926758,"speaker":"A"},{"text":"done","start":2253770,"end":2253970,"confidence":1,"speaker":"A"},{"text":"before","start":2253970,"end":2254290,"confidence":0.86816406,"speaker":"A"},{"text":"you","start":2254290,"end":2254610,"confidence":0.99902344,"speaker":"A"},{"text":"in","start":2254610,"end":2254810,"confidence":0.9550781,"speaker":"A"},{"text":"miskit","start":2254810,"end":2255490,"confidence":0.8145752,"speaker":"A"},{"text":"for","start":2255650,"end":2256010,"confidence":0.99902344,"speaker":"A"},{"text":"the","start":2256010,"end":2256250,"confidence":0.99902344,"speaker":"A"},{"text":"server","start":2256250,"end":2256530,"confidence":0.99902344,"speaker":"A"},{"text":"to","start":2256530,"end":2256690,"confidence":0.8510742,"speaker":"A"},{"text":"server","start":2256690,"end":2257050,"confidence":0.9995117,"speaker":"A"},{"text":"because","start":2257050,"end":2257250,"confidence":0.9995117,"speaker":"A"},{"text":"stuff","start":2257250,"end":2257490,"confidence":0.9991862,"speaker":"A"},{"text":"that","start":2257490,"end":2257650,"confidence":0.68603516,"speaker":"A"},{"text":"needs","start":2257650,"end":2257850,"confidence":0.9995117,"speaker":"A"},{"text":"to","start":2257850,"end":2257970,"confidence":1,"speaker":"A"},{"text":"be","start":2257970,"end":2258090,"confidence":1,"speaker":"A"},{"text":"signed,","start":2258090,"end":2258330,"confidence":0.79589844,"speaker":"A"},{"text":"etc.","start":2258330,"end":2259010,"confidence":0.88311,"speaker":"A"},{"text":"And","start":2259570,"end":2259849,"confidence":0.99609375,"speaker":"A"},{"text":"it","start":2259849,"end":2260010,"confidence":0.99902344,"speaker":"A"},{"text":"takes","start":2260010,"end":2260250,"confidence":1,"speaker":"A"},{"text":"care","start":2260250,"end":2260410,"confidence":1,"speaker":"A"},{"text":"of","start":2260410,"end":2260610,"confidence":1,"speaker":"A"},{"text":"all","start":2260610,"end":2260850,"confidence":0.9951172,"speaker":"A"},{"text":"that.","start":2260850,"end":2261170,"confidence":0.99560547,"speaker":"A"},{"text":"All","start":2261570,"end":2261890,"confidence":0.9902344,"speaker":"A"},{"text":"stuff","start":2261890,"end":2262170,"confidence":0.9947917,"speaker":"A"},{"text":"that","start":2262170,"end":2262450,"confidence":0.99853516,"speaker":"A"},{"text":"Claude","start":2262690,"end":2263330,"confidence":0.7474365,"speaker":"A"},{"text":"was","start":2263330,"end":2263650,"confidence":0.9995117,"speaker":"A"},{"text":"essentially","start":2263650,"end":2264210,"confidence":0.9995117,"speaker":"A"},{"text":"able","start":2264210,"end":2264450,"confidence":0.9980469,"speaker":"A"},{"text":"to","start":2264450,"end":2264770,"confidence":1,"speaker":"A"},{"text":"decipher","start":2264850,"end":2265610,"confidence":0.99593097,"speaker":"A"},{"text":"from","start":2265610,"end":2265970,"confidence":0.9995117,"speaker":"A"},{"text":"the","start":2266610,"end":2267010,"confidence":0.99072266,"speaker":"A"},{"text":"documentation.","start":2269340,"end":2270060,"confidence":0.9116211,"speaker":"A"}]},{"text":"There's one more thing I wanted to show.","start":2272620,"end":2274300,"confidence":0.9972331,"words":[{"text":"There's","start":2272620,"end":2273020,"confidence":0.9972331,"speaker":"A"},{"text":"one","start":2273020,"end":2273140,"confidence":1,"speaker":"A"},{"text":"more","start":2273140,"end":2273300,"confidence":1,"speaker":"A"},{"text":"thing","start":2273300,"end":2273460,"confidence":1,"speaker":"A"},{"text":"I","start":2273460,"end":2273620,"confidence":0.9995117,"speaker":"A"},{"text":"wanted","start":2273620,"end":2273860,"confidence":0.9975586,"speaker":"A"},{"text":"to","start":2273860,"end":2274020,"confidence":1,"speaker":"A"},{"text":"show.","start":2274020,"end":2274300,"confidence":0.99902344,"speaker":"A"}]},{"text":"If you want to hop in with a question while I pull something up, feel free.","start":2276380,"end":2280940,"confidence":0.9995117,"words":[{"text":"If","start":2276380,"end":2276660,"confidence":0.9995117,"speaker":"A"},{"text":"you","start":2276660,"end":2276780,"confidence":1,"speaker":"A"},{"text":"want","start":2276780,"end":2276860,"confidence":0.9921875,"speaker":"A"},{"text":"to","start":2276860,"end":2276980,"confidence":0.9995117,"speaker":"A"},{"text":"hop","start":2276980,"end":2277140,"confidence":0.99853516,"speaker":"A"},{"text":"in","start":2277140,"end":2277300,"confidence":0.9995117,"speaker":"A"},{"text":"with","start":2277300,"end":2277460,"confidence":1,"speaker":"A"},{"text":"a","start":2277460,"end":2277620,"confidence":0.9941406,"speaker":"A"},{"text":"question","start":2277620,"end":2277900,"confidence":1,"speaker":"A"},{"text":"while","start":2278380,"end":2278740,"confidence":0.9946289,"speaker":"A"},{"text":"I","start":2278740,"end":2279100,"confidence":0.99902344,"speaker":"A"},{"text":"pull","start":2279260,"end":2279620,"confidence":0.9995117,"speaker":"A"},{"text":"something","start":2279620,"end":2279860,"confidence":1,"speaker":"A"},{"text":"up,","start":2279860,"end":2280220,"confidence":0.99902344,"speaker":"A"},{"text":"feel","start":2280300,"end":2280620,"confidence":0.9995117,"speaker":"A"},{"text":"free.","start":2280620,"end":2280940,"confidence":1,"speaker":"A"}]},{"text":"No questions. Cool. So I'm going to show one last thing and that is how do we actually deploy this?","start":2301190,"end":2310310,"confidence":0.9892578,"words":[{"text":"No","start":2301190,"end":2301350,"confidence":0.9892578,"speaker":"A"},{"text":"questions.","start":2301350,"end":2301910,"confidence":0.9995117,"speaker":"A"},{"text":"Cool.","start":2303910,"end":2304390,"confidence":0.8347168,"speaker":"A"},{"text":"So","start":2304790,"end":2305030,"confidence":0.9921875,"speaker":"A"},{"text":"I'm","start":2305030,"end":2305190,"confidence":0.94905597,"speaker":"A"},{"text":"going","start":2305190,"end":2305270,"confidence":0.77441406,"speaker":"A"},{"text":"to","start":2305270,"end":2305350,"confidence":0.9980469,"speaker":"A"},{"text":"show","start":2305350,"end":2305510,"confidence":0.9975586,"speaker":"A"},{"text":"one","start":2305510,"end":2305710,"confidence":0.9995117,"speaker":"A"},{"text":"last","start":2305710,"end":2305950,"confidence":0.9995117,"speaker":"A"},{"text":"thing","start":2305950,"end":2306310,"confidence":0.99902344,"speaker":"A"},{"text":"and","start":2306950,"end":2307230,"confidence":0.9921875,"speaker":"A"},{"text":"that","start":2307230,"end":2307430,"confidence":0.9995117,"speaker":"A"},{"text":"is","start":2307430,"end":2307750,"confidence":0.99609375,"speaker":"A"},{"text":"how","start":2308230,"end":2308630,"confidence":0.9995117,"speaker":"A"},{"text":"do","start":2308710,"end":2308990,"confidence":0.99853516,"speaker":"A"},{"text":"we","start":2308990,"end":2309190,"confidence":1,"speaker":"A"},{"text":"actually","start":2309190,"end":2309470,"confidence":0.9970703,"speaker":"A"},{"text":"deploy","start":2309470,"end":2309990,"confidence":1,"speaker":"A"},{"text":"this?","start":2309990,"end":2310310,"confidence":0.9995117,"speaker":"A"}]},{"text":"Is this too big, too small? Looks okay. That looks good. Yeah, it looks good. Okay, cool.","start":2313350,"end":2320070,"confidence":0.9980469,"words":[{"text":"Is","start":2313350,"end":2313630,"confidence":0.9980469,"speaker":"A"},{"text":"this","start":2313630,"end":2313830,"confidence":0.9995117,"speaker":"A"},{"text":"too","start":2313830,"end":2314070,"confidence":0.9975586,"speaker":"A"},{"text":"big,","start":2314070,"end":2314350,"confidence":1,"speaker":"A"},{"text":"too","start":2314350,"end":2314590,"confidence":0.98779297,"speaker":"A"},{"text":"small?","start":2314590,"end":2314870,"confidence":0.99853516,"speaker":"A"},{"text":"Looks","start":2316150,"end":2316510,"confidence":0.8227539,"speaker":"A"},{"text":"okay.","start":2316510,"end":2316950,"confidence":0.9710286,"speaker":"A"},{"text":"That","start":2317590,"end":2317870,"confidence":0.97265625,"speaker":"C"},{"text":"looks","start":2317870,"end":2318150,"confidence":0.99902344,"speaker":"C"},{"text":"good.","start":2318150,"end":2318390,"confidence":0.9921875,"speaker":"C"},{"text":"Yeah,","start":2318710,"end":2319030,"confidence":0.992513,"speaker":"B"},{"text":"it","start":2319030,"end":2319110,"confidence":0.79003906,"speaker":"B"},{"text":"looks","start":2319110,"end":2319270,"confidence":0.99902344,"speaker":"B"},{"text":"good.","start":2319270,"end":2319430,"confidence":0.9951172,"speaker":"B"},{"text":"Okay,","start":2319430,"end":2319750,"confidence":0.9550781,"speaker":"A"},{"text":"cool.","start":2319750,"end":2320070,"confidence":0.99121094,"speaker":"A"}]},{"text":"So essentially what I've done is I'm using GitHub Actions. There's a way you can.","start":2323850,"end":2330410,"confidence":0.9604492,"words":[{"text":"So","start":2323850,"end":2324050,"confidence":0.9604492,"speaker":"A"},{"text":"essentially","start":2324050,"end":2324530,"confidence":0.9962158,"speaker":"A"},{"text":"what","start":2324530,"end":2324690,"confidence":0.9995117,"speaker":"A"},{"text":"I've","start":2324690,"end":2324930,"confidence":0.99886066,"speaker":"A"},{"text":"done","start":2324930,"end":2325210,"confidence":0.9995117,"speaker":"A"},{"text":"is","start":2325530,"end":2325930,"confidence":0.99365234,"speaker":"A"},{"text":"I'm","start":2326570,"end":2326930,"confidence":0.95214844,"speaker":"A"},{"text":"using","start":2326930,"end":2327210,"confidence":1,"speaker":"A"},{"text":"GitHub","start":2327370,"end":2327890,"confidence":0.9975586,"speaker":"A"},{"text":"Actions.","start":2327890,"end":2328490,"confidence":0.9992676,"speaker":"A"},{"text":"There's","start":2329290,"end":2329690,"confidence":0.9991862,"speaker":"A"},{"text":"a","start":2329690,"end":2329770,"confidence":0.9995117,"speaker":"A"},{"text":"way","start":2329770,"end":2329930,"confidence":1,"speaker":"A"},{"text":"you","start":2329930,"end":2330130,"confidence":0.99902344,"speaker":"A"},{"text":"can.","start":2330130,"end":2330410,"confidence":0.99902344,"speaker":"A"}]},{"text":"This is all public by the way, so I will provide URLs in the Slack or something. Let's do this one. So this is a Swift package for Bushel. It's called Bushel Cloud. It pulls the stuff up from.","start":2333130,"end":2350660,"confidence":0.99902344,"words":[{"text":"This","start":2333130,"end":2333410,"confidence":0.99902344,"speaker":"A"},{"text":"is","start":2333410,"end":2333530,"confidence":0.99853516,"speaker":"A"},{"text":"all","start":2333530,"end":2333770,"confidence":0.98876953,"speaker":"A"},{"text":"public","start":2334010,"end":2334370,"confidence":1,"speaker":"A"},{"text":"by","start":2334370,"end":2334570,"confidence":0.99902344,"speaker":"A"},{"text":"the","start":2334570,"end":2334690,"confidence":0.9995117,"speaker":"A"},{"text":"way,","start":2334690,"end":2334970,"confidence":1,"speaker":"A"},{"text":"so","start":2335050,"end":2335450,"confidence":0.9321289,"speaker":"A"},{"text":"I","start":2335850,"end":2336130,"confidence":0.99902344,"speaker":"A"},{"text":"will","start":2336130,"end":2336370,"confidence":0.86621094,"speaker":"A"},{"text":"provide","start":2336370,"end":2336689,"confidence":1,"speaker":"A"},{"text":"URLs","start":2336689,"end":2337330,"confidence":0.94067,"speaker":"A"},{"text":"in","start":2337330,"end":2337490,"confidence":0.98828125,"speaker":"A"},{"text":"the","start":2337490,"end":2337650,"confidence":0.9897461,"speaker":"A"},{"text":"Slack","start":2337650,"end":2337970,"confidence":0.998291,"speaker":"A"},{"text":"or","start":2337970,"end":2338170,"confidence":0.9970703,"speaker":"A"},{"text":"something.","start":2338170,"end":2338490,"confidence":0.9995117,"speaker":"A"},{"text":"Let's","start":2339450,"end":2339890,"confidence":0.99853516,"speaker":"A"},{"text":"do","start":2339890,"end":2340050,"confidence":0.9790039,"speaker":"A"},{"text":"this","start":2340050,"end":2340250,"confidence":0.9975586,"speaker":"A"},{"text":"one.","start":2340250,"end":2340570,"confidence":0.99316406,"speaker":"A"},{"text":"So","start":2342410,"end":2342810,"confidence":0.8173828,"speaker":"A"},{"text":"this","start":2343930,"end":2344210,"confidence":0.9995117,"speaker":"A"},{"text":"is","start":2344210,"end":2344370,"confidence":0.9995117,"speaker":"A"},{"text":"a","start":2344370,"end":2344530,"confidence":0.9765625,"speaker":"A"},{"text":"Swift","start":2344530,"end":2344810,"confidence":0.9226074,"speaker":"A"},{"text":"package","start":2344810,"end":2345370,"confidence":0.99768066,"speaker":"A"},{"text":"for","start":2347060,"end":2347220,"confidence":0.97998047,"speaker":"A"},{"text":"Bushel.","start":2347220,"end":2347860,"confidence":0.9685872,"speaker":"A"},{"text":"It's","start":2347860,"end":2348180,"confidence":0.9995117,"speaker":"A"},{"text":"called","start":2348180,"end":2348340,"confidence":0.99853516,"speaker":"A"},{"text":"Bushel","start":2348340,"end":2348780,"confidence":0.90283203,"speaker":"A"},{"text":"Cloud.","start":2348780,"end":2349180,"confidence":0.99658203,"speaker":"A"},{"text":"It","start":2349180,"end":2349420,"confidence":0.9995117,"speaker":"A"},{"text":"pulls","start":2349420,"end":2349700,"confidence":1,"speaker":"A"},{"text":"the","start":2349700,"end":2349820,"confidence":0.98828125,"speaker":"A"},{"text":"stuff","start":2349820,"end":2350060,"confidence":1,"speaker":"A"},{"text":"up","start":2350060,"end":2350300,"confidence":0.9995117,"speaker":"A"},{"text":"from.","start":2350300,"end":2350660,"confidence":0.9970703,"speaker":"A"}]},{"text":"Uses Miskit to go ahead and pull, get access to CloudKit and let me go back to the workflow. How familiar are you with GitHub workflows?","start":2351220,"end":2366580,"confidence":0.84887695,"words":[{"text":"Uses","start":2351220,"end":2351740,"confidence":0.84887695,"speaker":"A"},{"text":"Miskit","start":2351740,"end":2352340,"confidence":0.9329834,"speaker":"A"},{"text":"to","start":2353540,"end":2353820,"confidence":0.9941406,"speaker":"A"},{"text":"go","start":2353820,"end":2353980,"confidence":1,"speaker":"A"},{"text":"ahead","start":2353980,"end":2354260,"confidence":0.9995117,"speaker":"A"},{"text":"and","start":2354340,"end":2354740,"confidence":0.88720703,"speaker":"A"},{"text":"pull,","start":2356740,"end":2357220,"confidence":0.9621582,"speaker":"A"},{"text":"get","start":2357860,"end":2358140,"confidence":0.99902344,"speaker":"A"},{"text":"access","start":2358140,"end":2358380,"confidence":1,"speaker":"A"},{"text":"to","start":2358380,"end":2358700,"confidence":1,"speaker":"A"},{"text":"CloudKit","start":2358700,"end":2359460,"confidence":0.9325,"speaker":"A"},{"text":"and","start":2359940,"end":2360340,"confidence":0.98291016,"speaker":"A"},{"text":"let","start":2361060,"end":2361340,"confidence":0.99316406,"speaker":"A"},{"text":"me","start":2361340,"end":2361460,"confidence":1,"speaker":"A"},{"text":"go","start":2361460,"end":2361620,"confidence":0.9995117,"speaker":"A"},{"text":"back","start":2361620,"end":2361940,"confidence":1,"speaker":"A"},{"text":"to","start":2361940,"end":2362339,"confidence":0.9995117,"speaker":"A"},{"text":"the","start":2362339,"end":2362620,"confidence":1,"speaker":"A"},{"text":"workflow.","start":2362620,"end":2363300,"confidence":0.96276855,"speaker":"A"},{"text":"How","start":2364100,"end":2364420,"confidence":0.99853516,"speaker":"A"},{"text":"familiar","start":2364420,"end":2364860,"confidence":1,"speaker":"A"},{"text":"are","start":2364860,"end":2365020,"confidence":0.9995117,"speaker":"A"},{"text":"you","start":2365020,"end":2365180,"confidence":0.9995117,"speaker":"A"},{"text":"with","start":2365180,"end":2365380,"confidence":1,"speaker":"A"},{"text":"GitHub","start":2365380,"end":2365860,"confidence":0.87939453,"speaker":"A"},{"text":"workflows?","start":2365860,"end":2366580,"confidence":0.9026367,"speaker":"A"}]},{"text":"Sadly not had the chance to work too deeply with them yet. Okay. Basically it's like for CI, but you can also set it up on a schedule. So I did that and then it runs the scheduled job and then I just execute.","start":2369860,"end":2386490,"confidence":0.99576825,"words":[{"text":"Sadly","start":2369860,"end":2370300,"confidence":0.99576825,"speaker":"C"},{"text":"not","start":2370300,"end":2370500,"confidence":0.9951172,"speaker":"C"},{"text":"had","start":2370500,"end":2370660,"confidence":0.9980469,"speaker":"C"},{"text":"the","start":2370660,"end":2370780,"confidence":0.99658203,"speaker":"C"},{"text":"chance","start":2370780,"end":2371020,"confidence":0.99975586,"speaker":"C"},{"text":"to","start":2371020,"end":2371180,"confidence":0.9995117,"speaker":"C"},{"text":"work","start":2371180,"end":2371460,"confidence":1,"speaker":"C"},{"text":"too","start":2371780,"end":2372060,"confidence":0.99560547,"speaker":"C"},{"text":"deeply","start":2372060,"end":2372380,"confidence":0.9991862,"speaker":"C"},{"text":"with","start":2372380,"end":2372500,"confidence":0.9995117,"speaker":"C"},{"text":"them","start":2372500,"end":2372660,"confidence":0.97021484,"speaker":"C"},{"text":"yet.","start":2372660,"end":2372980,"confidence":0.98291016,"speaker":"C"},{"text":"Okay.","start":2373690,"end":2374090,"confidence":0.9503581,"speaker":"A"},{"text":"Basically","start":2375130,"end":2375610,"confidence":0.9987793,"speaker":"A"},{"text":"it's","start":2375610,"end":2375850,"confidence":0.99934894,"speaker":"A"},{"text":"like","start":2375850,"end":2375970,"confidence":0.99072266,"speaker":"A"},{"text":"for","start":2375970,"end":2376170,"confidence":0.9448242,"speaker":"A"},{"text":"CI,","start":2376170,"end":2376610,"confidence":0.97021484,"speaker":"A"},{"text":"but","start":2376610,"end":2376810,"confidence":0.99902344,"speaker":"A"},{"text":"you","start":2376810,"end":2376930,"confidence":0.9995117,"speaker":"A"},{"text":"can","start":2376930,"end":2377050,"confidence":0.9995117,"speaker":"A"},{"text":"also","start":2377050,"end":2377250,"confidence":0.9995117,"speaker":"A"},{"text":"set","start":2377250,"end":2377490,"confidence":1,"speaker":"A"},{"text":"it","start":2377490,"end":2377610,"confidence":0.9995117,"speaker":"A"},{"text":"up","start":2377610,"end":2377730,"confidence":0.9995117,"speaker":"A"},{"text":"on","start":2377730,"end":2377890,"confidence":0.9995117,"speaker":"A"},{"text":"a","start":2377890,"end":2378050,"confidence":0.9980469,"speaker":"A"},{"text":"schedule.","start":2378050,"end":2378570,"confidence":0.8905029,"speaker":"A"},{"text":"So","start":2378890,"end":2379170,"confidence":0.9941406,"speaker":"A"},{"text":"I","start":2379170,"end":2379330,"confidence":1,"speaker":"A"},{"text":"did","start":2379330,"end":2379530,"confidence":0.99902344,"speaker":"A"},{"text":"that","start":2379530,"end":2379850,"confidence":0.99902344,"speaker":"A"},{"text":"and","start":2381290,"end":2381570,"confidence":0.9902344,"speaker":"A"},{"text":"then","start":2381570,"end":2381850,"confidence":0.9980469,"speaker":"A"},{"text":"it","start":2382890,"end":2383170,"confidence":0.99853516,"speaker":"A"},{"text":"runs","start":2383170,"end":2383490,"confidence":0.99975586,"speaker":"A"},{"text":"the","start":2383490,"end":2383610,"confidence":0.6640625,"speaker":"A"},{"text":"scheduled","start":2383610,"end":2384090,"confidence":0.89404297,"speaker":"A"},{"text":"job","start":2384090,"end":2384410,"confidence":1,"speaker":"A"},{"text":"and","start":2384810,"end":2385090,"confidence":0.9975586,"speaker":"A"},{"text":"then","start":2385090,"end":2385250,"confidence":0.99902344,"speaker":"A"},{"text":"I","start":2385250,"end":2385450,"confidence":0.9995117,"speaker":"A"},{"text":"just","start":2385450,"end":2385730,"confidence":0.9995117,"speaker":"A"},{"text":"execute.","start":2385730,"end":2386490,"confidence":0.97875977,"speaker":"A"}]},{"text":"So then this was refactored over here into an action.","start":2390650,"end":2395210,"confidence":0.9941406,"words":[{"text":"So","start":2390650,"end":2390930,"confidence":0.9941406,"speaker":"A"},{"text":"then","start":2390930,"end":2391170,"confidence":0.9975586,"speaker":"A"},{"text":"this","start":2391170,"end":2391410,"confidence":1,"speaker":"A"},{"text":"was","start":2391410,"end":2391610,"confidence":0.9995117,"speaker":"A"},{"text":"refactored","start":2391610,"end":2392490,"confidence":0.99283856,"speaker":"A"},{"text":"over","start":2393290,"end":2393690,"confidence":0.99560547,"speaker":"A"},{"text":"here","start":2393690,"end":2394090,"confidence":0.9995117,"speaker":"A"},{"text":"into","start":2394330,"end":2394650,"confidence":0.9741211,"speaker":"A"},{"text":"an","start":2394650,"end":2394890,"confidence":0.99902344,"speaker":"A"},{"text":"action.","start":2394890,"end":2395210,"confidence":0.9995117,"speaker":"A"}]},{"text":"There we go. And I have all sorts of stuff here for like this is generic essentially, but all these, the environment, etc. These are all passed from that workflow into here. These are basically either API keys or the information that I need for accessing Cloud, the public, public database. Right.","start":2397770,"end":2426080,"confidence":0.89990234,"words":[{"text":"There","start":2397770,"end":2398090,"confidence":0.89990234,"speaker":"A"},{"text":"we","start":2398090,"end":2398250,"confidence":0.99853516,"speaker":"A"},{"text":"go.","start":2398250,"end":2398490,"confidence":0.99853516,"speaker":"A"},{"text":"And","start":2399540,"end":2399780,"confidence":0.9848633,"speaker":"A"},{"text":"I","start":2401140,"end":2401420,"confidence":0.99658203,"speaker":"A"},{"text":"have","start":2401420,"end":2401580,"confidence":0.9995117,"speaker":"A"},{"text":"all","start":2401580,"end":2401740,"confidence":0.9995117,"speaker":"A"},{"text":"sorts","start":2401740,"end":2402020,"confidence":0.890625,"speaker":"A"},{"text":"of","start":2402020,"end":2402180,"confidence":1,"speaker":"A"},{"text":"stuff","start":2402180,"end":2402380,"confidence":1,"speaker":"A"},{"text":"here","start":2402380,"end":2402660,"confidence":0.9995117,"speaker":"A"},{"text":"for","start":2403060,"end":2403460,"confidence":0.9863281,"speaker":"A"},{"text":"like","start":2405380,"end":2405780,"confidence":0.97021484,"speaker":"A"},{"text":"this","start":2406660,"end":2406940,"confidence":0.9975586,"speaker":"A"},{"text":"is","start":2406940,"end":2407100,"confidence":0.99902344,"speaker":"A"},{"text":"generic","start":2407100,"end":2407700,"confidence":1,"speaker":"A"},{"text":"essentially,","start":2407700,"end":2408420,"confidence":0.9996338,"speaker":"A"},{"text":"but","start":2408500,"end":2408900,"confidence":0.9941406,"speaker":"A"},{"text":"all","start":2410020,"end":2410300,"confidence":0.98828125,"speaker":"A"},{"text":"these,","start":2410300,"end":2410580,"confidence":0.9868164,"speaker":"A"},{"text":"the","start":2410820,"end":2411140,"confidence":0.9223633,"speaker":"A"},{"text":"environment,","start":2411140,"end":2411460,"confidence":1,"speaker":"A"},{"text":"etc.","start":2411700,"end":2412500,"confidence":0.975,"speaker":"A"},{"text":"These","start":2413140,"end":2413420,"confidence":0.9995117,"speaker":"A"},{"text":"are","start":2413420,"end":2413540,"confidence":0.9995117,"speaker":"A"},{"text":"all","start":2413540,"end":2413700,"confidence":0.99853516,"speaker":"A"},{"text":"passed","start":2413700,"end":2414060,"confidence":0.93310547,"speaker":"A"},{"text":"from","start":2414060,"end":2414220,"confidence":1,"speaker":"A"},{"text":"that","start":2414220,"end":2414420,"confidence":0.99902344,"speaker":"A"},{"text":"workflow","start":2414420,"end":2414980,"confidence":0.9741211,"speaker":"A"},{"text":"into","start":2414980,"end":2415260,"confidence":0.99609375,"speaker":"A"},{"text":"here.","start":2415260,"end":2415620,"confidence":0.99902344,"speaker":"A"},{"text":"These","start":2415940,"end":2416220,"confidence":0.9975586,"speaker":"A"},{"text":"are","start":2416220,"end":2416380,"confidence":0.9995117,"speaker":"A"},{"text":"basically","start":2416380,"end":2416820,"confidence":0.9992676,"speaker":"A"},{"text":"either","start":2416820,"end":2417180,"confidence":0.9995117,"speaker":"A"},{"text":"API","start":2417180,"end":2417620,"confidence":0.85180664,"speaker":"A"},{"text":"keys","start":2417620,"end":2417980,"confidence":0.99975586,"speaker":"A"},{"text":"or","start":2417980,"end":2418180,"confidence":0.9995117,"speaker":"A"},{"text":"the","start":2418180,"end":2418420,"confidence":0.99902344,"speaker":"A"},{"text":"information","start":2418420,"end":2418740,"confidence":0.9995117,"speaker":"A"},{"text":"that","start":2418820,"end":2419100,"confidence":0.99902344,"speaker":"A"},{"text":"I","start":2419100,"end":2419260,"confidence":1,"speaker":"A"},{"text":"need","start":2419260,"end":2419540,"confidence":0.9995117,"speaker":"A"},{"text":"for","start":2419620,"end":2420020,"confidence":0.9995117,"speaker":"A"},{"text":"accessing","start":2420500,"end":2421100,"confidence":0.9953613,"speaker":"A"},{"text":"Cloud,","start":2421100,"end":2421460,"confidence":0.9243164,"speaker":"A"},{"text":"the","start":2421460,"end":2421780,"confidence":0.8491211,"speaker":"A"},{"text":"public,","start":2421780,"end":2422100,"confidence":0.765625,"speaker":"A"},{"text":"public","start":2424020,"end":2424380,"confidence":0.9995117,"speaker":"A"},{"text":"database.","start":2424380,"end":2425060,"confidence":0.99869794,"speaker":"A"},{"text":"Right.","start":2425840,"end":2426080,"confidence":0.9008789,"speaker":"A"}]},{"text":"And then I already pre built the binary. So we already have that. We're running this on Ubuntu because it's the default. Look at it. If there is no binary, it goes ahead and builds the binary for me.","start":2426480,"end":2443840,"confidence":0.9794922,"words":[{"text":"And","start":2426480,"end":2426760,"confidence":0.9794922,"speaker":"A"},{"text":"then","start":2426760,"end":2427040,"confidence":0.9980469,"speaker":"A"},{"text":"I","start":2427840,"end":2428120,"confidence":0.96435547,"speaker":"A"},{"text":"already","start":2428120,"end":2428360,"confidence":0.99902344,"speaker":"A"},{"text":"pre","start":2428360,"end":2428680,"confidence":0.99853516,"speaker":"A"},{"text":"built","start":2428680,"end":2429200,"confidence":0.8404948,"speaker":"A"},{"text":"the","start":2429760,"end":2430160,"confidence":0.9970703,"speaker":"A"},{"text":"binary.","start":2430160,"end":2430880,"confidence":0.9977214,"speaker":"A"},{"text":"So","start":2431120,"end":2431520,"confidence":0.99316406,"speaker":"A"},{"text":"we","start":2431600,"end":2431880,"confidence":0.9995117,"speaker":"A"},{"text":"already","start":2431880,"end":2432040,"confidence":0.9995117,"speaker":"A"},{"text":"have","start":2432040,"end":2432200,"confidence":0.99902344,"speaker":"A"},{"text":"that.","start":2432200,"end":2432360,"confidence":1,"speaker":"A"},{"text":"We're","start":2432360,"end":2432600,"confidence":0.9973958,"speaker":"A"},{"text":"running","start":2432600,"end":2432840,"confidence":1,"speaker":"A"},{"text":"this","start":2432840,"end":2433120,"confidence":0.99902344,"speaker":"A"},{"text":"on","start":2433200,"end":2433600,"confidence":0.9975586,"speaker":"A"},{"text":"Ubuntu","start":2434880,"end":2435720,"confidence":0.93408203,"speaker":"A"},{"text":"because","start":2435720,"end":2435960,"confidence":0.94970703,"speaker":"A"},{"text":"it's","start":2435960,"end":2436160,"confidence":0.99902344,"speaker":"A"},{"text":"the","start":2436160,"end":2436280,"confidence":0.8647461,"speaker":"A"},{"text":"default.","start":2436280,"end":2436800,"confidence":0.9998779,"speaker":"A"},{"text":"Look","start":2437200,"end":2437480,"confidence":0.9970703,"speaker":"A"},{"text":"at","start":2437480,"end":2437640,"confidence":0.9995117,"speaker":"A"},{"text":"it.","start":2437640,"end":2437920,"confidence":0.9995117,"speaker":"A"},{"text":"If","start":2439200,"end":2439600,"confidence":0.9980469,"speaker":"A"},{"text":"there","start":2439920,"end":2440280,"confidence":1,"speaker":"A"},{"text":"is","start":2440280,"end":2440560,"confidence":0.9995117,"speaker":"A"},{"text":"no","start":2440560,"end":2440880,"confidence":0.9970703,"speaker":"A"},{"text":"binary,","start":2440960,"end":2441639,"confidence":0.9977214,"speaker":"A"},{"text":"it","start":2441639,"end":2441840,"confidence":0.9736328,"speaker":"A"},{"text":"goes","start":2441840,"end":2442000,"confidence":1,"speaker":"A"},{"text":"ahead","start":2442000,"end":2442120,"confidence":0.99853516,"speaker":"A"},{"text":"and","start":2442120,"end":2442320,"confidence":1,"speaker":"A"},{"text":"builds","start":2442320,"end":2442680,"confidence":0.99853516,"speaker":"A"},{"text":"the","start":2442680,"end":2442800,"confidence":1,"speaker":"A"},{"text":"binary","start":2442800,"end":2443280,"confidence":0.9991862,"speaker":"A"},{"text":"for","start":2443280,"end":2443520,"confidence":0.99853516,"speaker":"A"},{"text":"me.","start":2443520,"end":2443840,"confidence":0.9995117,"speaker":"A"}]},{"text":"So that's what this is doing. And then we make sure the binary works. We make, we make it executable, we validate, make sure all the API secrets are there. We then go ahead and this validates the pim. But essentially this is the fun part.","start":2444000,"end":2462370,"confidence":0.95166016,"words":[{"text":"So","start":2444000,"end":2444240,"confidence":0.95166016,"speaker":"A"},{"text":"that's","start":2444240,"end":2444400,"confidence":0.9991862,"speaker":"A"},{"text":"what","start":2444400,"end":2444520,"confidence":0.9995117,"speaker":"A"},{"text":"this","start":2444520,"end":2444680,"confidence":1,"speaker":"A"},{"text":"is","start":2444680,"end":2444880,"confidence":1,"speaker":"A"},{"text":"doing.","start":2444880,"end":2445200,"confidence":0.99902344,"speaker":"A"},{"text":"And","start":2447120,"end":2447440,"confidence":0.88671875,"speaker":"A"},{"text":"then","start":2447440,"end":2447760,"confidence":0.99902344,"speaker":"A"},{"text":"we","start":2448800,"end":2449080,"confidence":0.9995117,"speaker":"A"},{"text":"make","start":2449080,"end":2449280,"confidence":0.7973633,"speaker":"A"},{"text":"sure","start":2449280,"end":2449480,"confidence":1,"speaker":"A"},{"text":"the","start":2449480,"end":2449640,"confidence":0.9941406,"speaker":"A"},{"text":"binary","start":2449640,"end":2450080,"confidence":0.92838544,"speaker":"A"},{"text":"works.","start":2450080,"end":2450640,"confidence":0.9995117,"speaker":"A"},{"text":"We","start":2450880,"end":2451120,"confidence":0.41552734,"speaker":"A"},{"text":"make,","start":2451120,"end":2451180,"confidence":0.6088867,"speaker":"A"},{"text":"we","start":2451250,"end":2451330,"confidence":0.6176758,"speaker":"A"},{"text":"make","start":2451330,"end":2451450,"confidence":0.99902344,"speaker":"A"},{"text":"it","start":2451450,"end":2451610,"confidence":0.9550781,"speaker":"A"},{"text":"executable,","start":2451610,"end":2452210,"confidence":0.9968262,"speaker":"A"},{"text":"we","start":2452290,"end":2452650,"confidence":0.99658203,"speaker":"A"},{"text":"validate,","start":2452650,"end":2453290,"confidence":0.9996745,"speaker":"A"},{"text":"make","start":2453290,"end":2453530,"confidence":0.9951172,"speaker":"A"},{"text":"sure","start":2453530,"end":2453730,"confidence":1,"speaker":"A"},{"text":"all","start":2453730,"end":2454050,"confidence":0.99902344,"speaker":"A"},{"text":"the","start":2454050,"end":2454450,"confidence":0.99902344,"speaker":"A"},{"text":"API","start":2455010,"end":2455570,"confidence":0.9987793,"speaker":"A"},{"text":"secrets","start":2455570,"end":2456050,"confidence":0.98339844,"speaker":"A"},{"text":"are","start":2456050,"end":2456250,"confidence":0.99902344,"speaker":"A"},{"text":"there.","start":2456250,"end":2456530,"confidence":0.99902344,"speaker":"A"},{"text":"We","start":2457650,"end":2457970,"confidence":0.9951172,"speaker":"A"},{"text":"then","start":2457970,"end":2458210,"confidence":0.99658203,"speaker":"A"},{"text":"go","start":2458210,"end":2458410,"confidence":0.99853516,"speaker":"A"},{"text":"ahead","start":2458410,"end":2458690,"confidence":0.99853516,"speaker":"A"},{"text":"and","start":2458930,"end":2459290,"confidence":0.9921875,"speaker":"A"},{"text":"this","start":2459290,"end":2459530,"confidence":0.9863281,"speaker":"A"},{"text":"validates","start":2459530,"end":2460010,"confidence":0.99690753,"speaker":"A"},{"text":"the","start":2460010,"end":2460170,"confidence":0.99902344,"speaker":"A"},{"text":"pim.","start":2460170,"end":2460530,"confidence":0.8864746,"speaker":"A"},{"text":"But","start":2460690,"end":2460970,"confidence":0.99853516,"speaker":"A"},{"text":"essentially","start":2460970,"end":2461370,"confidence":0.9954834,"speaker":"A"},{"text":"this","start":2461370,"end":2461530,"confidence":0.9902344,"speaker":"A"},{"text":"is","start":2461530,"end":2461650,"confidence":0.9814453,"speaker":"A"},{"text":"the","start":2461650,"end":2461770,"confidence":0.8173828,"speaker":"A"},{"text":"fun","start":2461770,"end":2462010,"confidence":0.9980469,"speaker":"A"},{"text":"part.","start":2462010,"end":2462370,"confidence":0.9995117,"speaker":"A"}]},{"text":"We go ahead, we have all our inputs for the private key, the key id, environment, container id. And then I use Virtual Buddy for signing verification. And.","start":2463410,"end":2474450,"confidence":0.9995117,"words":[{"text":"We","start":2463410,"end":2463690,"confidence":0.9995117,"speaker":"A"},{"text":"go","start":2463690,"end":2463810,"confidence":0.9995117,"speaker":"A"},{"text":"ahead,","start":2463810,"end":2464050,"confidence":0.99902344,"speaker":"A"},{"text":"we","start":2464050,"end":2464330,"confidence":0.9980469,"speaker":"A"},{"text":"have","start":2464330,"end":2464610,"confidence":0.99902344,"speaker":"A"},{"text":"all","start":2464930,"end":2465290,"confidence":0.99853516,"speaker":"A"},{"text":"our","start":2465290,"end":2465530,"confidence":0.99365234,"speaker":"A"},{"text":"inputs","start":2465530,"end":2466010,"confidence":0.88171387,"speaker":"A"},{"text":"for","start":2466010,"end":2466170,"confidence":0.99902344,"speaker":"A"},{"text":"the","start":2466170,"end":2466290,"confidence":1,"speaker":"A"},{"text":"private","start":2466290,"end":2466490,"confidence":0.99902344,"speaker":"A"},{"text":"key,","start":2466490,"end":2466770,"confidence":0.9995117,"speaker":"A"},{"text":"the","start":2466770,"end":2467089,"confidence":0.9277344,"speaker":"A"},{"text":"key","start":2467089,"end":2467410,"confidence":0.98779297,"speaker":"A"},{"text":"id,","start":2467410,"end":2467730,"confidence":0.97021484,"speaker":"A"},{"text":"environment,","start":2467810,"end":2468210,"confidence":0.99902344,"speaker":"A"},{"text":"container","start":2468690,"end":2469290,"confidence":0.99902344,"speaker":"A"},{"text":"id.","start":2469290,"end":2469570,"confidence":0.99609375,"speaker":"A"},{"text":"And","start":2470610,"end":2470890,"confidence":0.9707031,"speaker":"A"},{"text":"then","start":2470890,"end":2471050,"confidence":0.99902344,"speaker":"A"},{"text":"I","start":2471050,"end":2471170,"confidence":0.99902344,"speaker":"A"},{"text":"use","start":2471170,"end":2471370,"confidence":0.99658203,"speaker":"A"},{"text":"Virtual","start":2471370,"end":2471770,"confidence":0.9996338,"speaker":"A"},{"text":"Buddy","start":2471770,"end":2472090,"confidence":0.98583984,"speaker":"A"},{"text":"for","start":2472090,"end":2472250,"confidence":0.99902344,"speaker":"A"},{"text":"signing","start":2472250,"end":2472650,"confidence":0.9938965,"speaker":"A"},{"text":"verification.","start":2472650,"end":2473410,"confidence":0.99990237,"speaker":"A"},{"text":"And.","start":2474050,"end":2474450,"confidence":0.93603516,"speaker":"A"}]},{"text":"It then goes in and it runs the sync and then we'll go in. Basically it pulls from several websites information about macrosos, restore images and checks whether they're signed. And then it goes ahead and it adds those to the database. And then what this does is it exports the information in a run. Let's, let's take a look, see if I have one.","start":2478460,"end":2504020,"confidence":0.9707031,"words":[{"text":"It","start":2478460,"end":2478580,"confidence":0.9707031,"speaker":"A"},{"text":"then","start":2478580,"end":2478740,"confidence":0.9980469,"speaker":"A"},{"text":"goes","start":2478740,"end":2479060,"confidence":0.99975586,"speaker":"A"},{"text":"in","start":2479060,"end":2479220,"confidence":0.99902344,"speaker":"A"},{"text":"and","start":2479220,"end":2479500,"confidence":0.8173828,"speaker":"A"},{"text":"it","start":2479900,"end":2480300,"confidence":0.99560547,"speaker":"A"},{"text":"runs","start":2481260,"end":2481740,"confidence":1,"speaker":"A"},{"text":"the","start":2481740,"end":2481940,"confidence":0.9995117,"speaker":"A"},{"text":"sync","start":2481940,"end":2482380,"confidence":0.9733073,"speaker":"A"},{"text":"and","start":2483500,"end":2483780,"confidence":0.96435547,"speaker":"A"},{"text":"then","start":2483780,"end":2484060,"confidence":0.97753906,"speaker":"A"},{"text":"we'll","start":2484860,"end":2485220,"confidence":0.8601888,"speaker":"A"},{"text":"go","start":2485220,"end":2485380,"confidence":0.99902344,"speaker":"A"},{"text":"in.","start":2485380,"end":2485660,"confidence":0.9980469,"speaker":"A"},{"text":"Basically","start":2485980,"end":2486460,"confidence":0.9995117,"speaker":"A"},{"text":"it","start":2486460,"end":2486620,"confidence":0.95996094,"speaker":"A"},{"text":"pulls","start":2486620,"end":2486900,"confidence":0.99902344,"speaker":"A"},{"text":"from","start":2486900,"end":2487060,"confidence":1,"speaker":"A"},{"text":"several","start":2487060,"end":2487340,"confidence":0.9995117,"speaker":"A"},{"text":"websites","start":2487340,"end":2488140,"confidence":0.99658203,"speaker":"A"},{"text":"information","start":2489100,"end":2489500,"confidence":1,"speaker":"A"},{"text":"about","start":2489580,"end":2489900,"confidence":0.9995117,"speaker":"A"},{"text":"macrosos,","start":2489900,"end":2490500,"confidence":0.85645,"speaker":"A"},{"text":"restore","start":2490500,"end":2490940,"confidence":0.85498047,"speaker":"A"},{"text":"images","start":2490940,"end":2491380,"confidence":0.998291,"speaker":"A"},{"text":"and","start":2491380,"end":2491620,"confidence":0.9980469,"speaker":"A"},{"text":"checks","start":2491620,"end":2491940,"confidence":0.9996745,"speaker":"A"},{"text":"whether","start":2491940,"end":2492100,"confidence":0.99902344,"speaker":"A"},{"text":"they're","start":2492100,"end":2492380,"confidence":0.98030597,"speaker":"A"},{"text":"signed.","start":2492380,"end":2492939,"confidence":0.80981445,"speaker":"A"},{"text":"And","start":2493340,"end":2493620,"confidence":0.94970703,"speaker":"A"},{"text":"then","start":2493620,"end":2493780,"confidence":0.9970703,"speaker":"A"},{"text":"it","start":2493780,"end":2493940,"confidence":1,"speaker":"A"},{"text":"goes","start":2493940,"end":2494140,"confidence":1,"speaker":"A"},{"text":"ahead","start":2494140,"end":2494340,"confidence":0.99902344,"speaker":"A"},{"text":"and","start":2494340,"end":2494700,"confidence":0.53125,"speaker":"A"},{"text":"it","start":2494780,"end":2495180,"confidence":0.86621094,"speaker":"A"},{"text":"adds","start":2496380,"end":2496900,"confidence":0.99853516,"speaker":"A"},{"text":"those","start":2496900,"end":2497180,"confidence":0.9995117,"speaker":"A"},{"text":"to","start":2497260,"end":2497540,"confidence":1,"speaker":"A"},{"text":"the","start":2497540,"end":2497660,"confidence":1,"speaker":"A"},{"text":"database.","start":2497660,"end":2498260,"confidence":0.9998372,"speaker":"A"},{"text":"And","start":2498260,"end":2498500,"confidence":0.9238281,"speaker":"A"},{"text":"then","start":2498500,"end":2498700,"confidence":0.9902344,"speaker":"A"},{"text":"what","start":2498700,"end":2498900,"confidence":0.9995117,"speaker":"A"},{"text":"this","start":2498900,"end":2499060,"confidence":1,"speaker":"A"},{"text":"does","start":2499060,"end":2499260,"confidence":0.9995117,"speaker":"A"},{"text":"is","start":2499260,"end":2499460,"confidence":0.99902344,"speaker":"A"},{"text":"it","start":2499460,"end":2499620,"confidence":0.86279297,"speaker":"A"},{"text":"exports","start":2499620,"end":2500140,"confidence":0.99902344,"speaker":"A"},{"text":"the","start":2500620,"end":2500940,"confidence":0.99560547,"speaker":"A"},{"text":"information","start":2500940,"end":2501260,"confidence":1,"speaker":"A"},{"text":"in","start":2501500,"end":2501780,"confidence":0.9946289,"speaker":"A"},{"text":"a","start":2501780,"end":2501900,"confidence":0.98046875,"speaker":"A"},{"text":"run.","start":2501900,"end":2502100,"confidence":0.9926758,"speaker":"A"},{"text":"Let's,","start":2502100,"end":2502460,"confidence":0.7273763,"speaker":"A"},{"text":"let's","start":2502460,"end":2502700,"confidence":0.8728841,"speaker":"A"},{"text":"take","start":2502700,"end":2502820,"confidence":0.9921875,"speaker":"A"},{"text":"a","start":2502820,"end":2502940,"confidence":1,"speaker":"A"},{"text":"look,","start":2502940,"end":2503140,"confidence":0.9995117,"speaker":"A"},{"text":"see","start":2503140,"end":2503380,"confidence":0.99902344,"speaker":"A"},{"text":"if","start":2503380,"end":2503500,"confidence":1,"speaker":"A"},{"text":"I","start":2503500,"end":2503580,"confidence":0.9995117,"speaker":"A"},{"text":"have","start":2503580,"end":2503740,"confidence":0.9995117,"speaker":"A"},{"text":"one.","start":2503740,"end":2504020,"confidence":0.9863281,"speaker":"A"}]},{"text":"I can show you. Oh, there's one scheduled.","start":2504020,"end":2507420,"confidence":0.99316406,"words":[{"text":"I","start":2504020,"end":2504260,"confidence":0.99316406,"speaker":"A"},{"text":"can","start":2504260,"end":2504420,"confidence":0.9458008,"speaker":"A"},{"text":"show","start":2504420,"end":2504580,"confidence":0.9995117,"speaker":"A"},{"text":"you.","start":2504580,"end":2504860,"confidence":0.9970703,"speaker":"A"},{"text":"Oh,","start":2505980,"end":2506180,"confidence":0.8977051,"speaker":"A"},{"text":"there's","start":2506180,"end":2506460,"confidence":0.91503906,"speaker":"A"},{"text":"one","start":2506460,"end":2506700,"confidence":0.99853516,"speaker":"A"},{"text":"scheduled.","start":2506700,"end":2507420,"confidence":0.97436523,"speaker":"A"}]},{"text":"Yeah, here we go. So there's 57 new restore images created, 177 updated. 234 Total. No operations failed. I also store Xcode versions and Swift versions.","start":2510060,"end":2525900,"confidence":0.97347003,"words":[{"text":"Yeah,","start":2510060,"end":2510460,"confidence":0.97347003,"speaker":"A"},{"text":"here","start":2510460,"end":2510660,"confidence":0.9995117,"speaker":"A"},{"text":"we","start":2510660,"end":2510780,"confidence":1,"speaker":"A"},{"text":"go.","start":2510780,"end":2511020,"confidence":0.9995117,"speaker":"A"},{"text":"So","start":2511260,"end":2511660,"confidence":0.8173828,"speaker":"A"},{"text":"there's","start":2512060,"end":2512700,"confidence":0.9090169,"speaker":"A"},{"text":"57","start":2513100,"end":2513700,"confidence":0.99829,"speaker":"A"},{"text":"new","start":2513700,"end":2514060,"confidence":0.98291016,"speaker":"A"},{"text":"restore","start":2514060,"end":2514580,"confidence":0.84936523,"speaker":"A"},{"text":"images","start":2514580,"end":2514980,"confidence":0.9980469,"speaker":"A"},{"text":"created,","start":2514980,"end":2515580,"confidence":0.9970703,"speaker":"A"},{"text":"177","start":2516300,"end":2517500,"confidence":0.95771,"speaker":"A"},{"text":"updated.","start":2517660,"end":2518300,"confidence":0.9980469,"speaker":"A"},{"text":"234","start":2518780,"end":2519900,"confidence":0.93447,"speaker":"A"},{"text":"total.","start":2519980,"end":2520380,"confidence":0.9995117,"speaker":"A"},{"text":"No","start":2521420,"end":2521740,"confidence":0.9970703,"speaker":"A"},{"text":"operations","start":2521740,"end":2522300,"confidence":0.9987793,"speaker":"A"},{"text":"failed.","start":2522380,"end":2523020,"confidence":0.9992676,"speaker":"A"},{"text":"I","start":2523100,"end":2523380,"confidence":0.9916992,"speaker":"A"},{"text":"also","start":2523380,"end":2523580,"confidence":0.99902344,"speaker":"A"},{"text":"store","start":2523580,"end":2523900,"confidence":0.77490234,"speaker":"A"},{"text":"Xcode","start":2523900,"end":2524340,"confidence":0.89245605,"speaker":"A"},{"text":"versions","start":2524340,"end":2524700,"confidence":0.9970703,"speaker":"A"},{"text":"and","start":2524700,"end":2524980,"confidence":0.9370117,"speaker":"A"},{"text":"Swift","start":2524980,"end":2525420,"confidence":0.9921875,"speaker":"A"},{"text":"versions.","start":2525420,"end":2525900,"confidence":0.9975586,"speaker":"A"}]},{"text":"Those get stored as well. Had to rebuild it, but here is the results. I'm not going to pull that up, but it's essentially updated my CloudKit database and that's all in the public database. And then maybe even by the time I present this, I'll have a working example in Bushel with that example working, which would be awesome. Celestra, same idea.","start":2526780,"end":2554870,"confidence":0.99853516,"words":[{"text":"Those","start":2526780,"end":2527100,"confidence":0.99853516,"speaker":"A"},{"text":"get","start":2527100,"end":2527300,"confidence":0.99902344,"speaker":"A"},{"text":"stored","start":2527300,"end":2527620,"confidence":0.99853516,"speaker":"A"},{"text":"as","start":2527620,"end":2527780,"confidence":0.9995117,"speaker":"A"},{"text":"well.","start":2527780,"end":2528060,"confidence":0.9995117,"speaker":"A"},{"text":"Had","start":2529420,"end":2529700,"confidence":0.89697266,"speaker":"A"},{"text":"to","start":2529700,"end":2529860,"confidence":0.9736328,"speaker":"A"},{"text":"rebuild","start":2529860,"end":2530180,"confidence":0.9995117,"speaker":"A"},{"text":"it,","start":2530180,"end":2530460,"confidence":0.9975586,"speaker":"A"},{"text":"but","start":2530630,"end":2530790,"confidence":0.99902344,"speaker":"A"},{"text":"here","start":2530790,"end":2531070,"confidence":1,"speaker":"A"},{"text":"is","start":2531070,"end":2531310,"confidence":0.99902344,"speaker":"A"},{"text":"the","start":2531310,"end":2531510,"confidence":1,"speaker":"A"},{"text":"results.","start":2531510,"end":2531830,"confidence":0.98046875,"speaker":"A"},{"text":"I'm","start":2533750,"end":2534070,"confidence":0.9995117,"speaker":"A"},{"text":"not","start":2534070,"end":2534190,"confidence":0.9995117,"speaker":"A"},{"text":"going","start":2534190,"end":2534310,"confidence":0.9140625,"speaker":"A"},{"text":"to","start":2534310,"end":2534390,"confidence":0.9995117,"speaker":"A"},{"text":"pull","start":2534390,"end":2534590,"confidence":0.99975586,"speaker":"A"},{"text":"that","start":2534590,"end":2534750,"confidence":0.99853516,"speaker":"A"},{"text":"up,","start":2534750,"end":2535030,"confidence":0.9995117,"speaker":"A"},{"text":"but","start":2535830,"end":2536110,"confidence":0.9995117,"speaker":"A"},{"text":"it's","start":2536110,"end":2536350,"confidence":0.9944661,"speaker":"A"},{"text":"essentially","start":2536350,"end":2536950,"confidence":0.9980469,"speaker":"A"},{"text":"updated","start":2537270,"end":2537750,"confidence":0.99853516,"speaker":"A"},{"text":"my","start":2537750,"end":2537990,"confidence":0.99609375,"speaker":"A"},{"text":"CloudKit","start":2537990,"end":2538710,"confidence":0.9953613,"speaker":"A"},{"text":"database","start":2538790,"end":2539510,"confidence":0.99902344,"speaker":"A"},{"text":"and","start":2542070,"end":2542470,"confidence":0.99658203,"speaker":"A"},{"text":"that's","start":2542550,"end":2542950,"confidence":0.9998372,"speaker":"A"},{"text":"all","start":2542950,"end":2543070,"confidence":0.9995117,"speaker":"A"},{"text":"in","start":2543070,"end":2543190,"confidence":0.9892578,"speaker":"A"},{"text":"the","start":2543190,"end":2543310,"confidence":0.99902344,"speaker":"A"},{"text":"public","start":2543310,"end":2543510,"confidence":1,"speaker":"A"},{"text":"database.","start":2543510,"end":2544030,"confidence":0.9991862,"speaker":"A"},{"text":"And","start":2544030,"end":2544150,"confidence":0.9980469,"speaker":"A"},{"text":"then","start":2544150,"end":2544390,"confidence":0.9980469,"speaker":"A"},{"text":"maybe","start":2545110,"end":2545470,"confidence":0.99975586,"speaker":"A"},{"text":"even","start":2545470,"end":2545670,"confidence":0.9995117,"speaker":"A"},{"text":"by","start":2545670,"end":2545870,"confidence":0.9995117,"speaker":"A"},{"text":"the","start":2545870,"end":2546030,"confidence":0.9995117,"speaker":"A"},{"text":"time","start":2546030,"end":2546190,"confidence":1,"speaker":"A"},{"text":"I","start":2546190,"end":2546310,"confidence":0.99560547,"speaker":"A"},{"text":"present","start":2546310,"end":2546550,"confidence":0.9995117,"speaker":"A"},{"text":"this,","start":2546550,"end":2546869,"confidence":0.9995117,"speaker":"A"},{"text":"I'll","start":2546869,"end":2547110,"confidence":0.99902344,"speaker":"A"},{"text":"have","start":2547110,"end":2547310,"confidence":0.99853516,"speaker":"A"},{"text":"a","start":2547310,"end":2547550,"confidence":0.97314453,"speaker":"A"},{"text":"working","start":2547550,"end":2547830,"confidence":0.99902344,"speaker":"A"},{"text":"example","start":2547830,"end":2548350,"confidence":0.9814453,"speaker":"A"},{"text":"in","start":2548350,"end":2548510,"confidence":0.7578125,"speaker":"A"},{"text":"Bushel","start":2548510,"end":2548950,"confidence":0.9241536,"speaker":"A"},{"text":"with","start":2548950,"end":2549150,"confidence":1,"speaker":"A"},{"text":"that","start":2549150,"end":2549390,"confidence":0.9975586,"speaker":"A"},{"text":"example","start":2549390,"end":2549910,"confidence":0.9869792,"speaker":"A"},{"text":"working,","start":2549910,"end":2550230,"confidence":0.99902344,"speaker":"A"},{"text":"which","start":2550630,"end":2550910,"confidence":0.93310547,"speaker":"A"},{"text":"would","start":2550910,"end":2551070,"confidence":0.9277344,"speaker":"A"},{"text":"be","start":2551070,"end":2551230,"confidence":0.9995117,"speaker":"A"},{"text":"awesome.","start":2551230,"end":2551670,"confidence":0.99886066,"speaker":"A"},{"text":"Celestra,","start":2552870,"end":2553750,"confidence":0.7898763,"speaker":"A"},{"text":"same","start":2553990,"end":2554310,"confidence":0.99853516,"speaker":"A"},{"text":"idea.","start":2554310,"end":2554870,"confidence":0.998291,"speaker":"A"}]},{"text":"So this looks like it was a RSS update. We get the workflow file and. Oh, sorry, I should point out, because you're probably wondering where is all these. The stuff all these secrets stored? Yes, they are stored in Actions secrets right here.","start":2555030,"end":2573070,"confidence":0.9970703,"words":[{"text":"So","start":2555030,"end":2555310,"confidence":0.9970703,"speaker":"A"},{"text":"this","start":2555310,"end":2555470,"confidence":0.9916992,"speaker":"A"},{"text":"looks","start":2555470,"end":2555670,"confidence":0.99975586,"speaker":"A"},{"text":"like","start":2555670,"end":2555790,"confidence":0.9995117,"speaker":"A"},{"text":"it","start":2555790,"end":2555910,"confidence":0.9824219,"speaker":"A"},{"text":"was","start":2555910,"end":2555990,"confidence":0.9975586,"speaker":"A"},{"text":"a","start":2555990,"end":2556110,"confidence":0.80810547,"speaker":"A"},{"text":"RSS","start":2556110,"end":2556630,"confidence":0.72924805,"speaker":"A"},{"text":"update.","start":2556630,"end":2557190,"confidence":0.9975586,"speaker":"A"},{"text":"We","start":2558910,"end":2559030,"confidence":0.9663086,"speaker":"A"},{"text":"get","start":2559030,"end":2559150,"confidence":0.5415039,"speaker":"A"},{"text":"the","start":2559150,"end":2559270,"confidence":0.9970703,"speaker":"A"},{"text":"workflow","start":2559270,"end":2559790,"confidence":0.9992676,"speaker":"A"},{"text":"file","start":2559790,"end":2560190,"confidence":0.79589844,"speaker":"A"},{"text":"and.","start":2562510,"end":2562830,"confidence":0.8984375,"speaker":"A"},{"text":"Oh,","start":2562830,"end":2563150,"confidence":0.78930664,"speaker":"A"},{"text":"sorry,","start":2563150,"end":2563430,"confidence":0.9995117,"speaker":"A"},{"text":"I","start":2563430,"end":2563590,"confidence":0.99902344,"speaker":"A"},{"text":"should","start":2563590,"end":2563830,"confidence":0.9995117,"speaker":"A"},{"text":"point","start":2563830,"end":2564070,"confidence":1,"speaker":"A"},{"text":"out,","start":2564070,"end":2564270,"confidence":1,"speaker":"A"},{"text":"because","start":2564270,"end":2564470,"confidence":0.96191406,"speaker":"A"},{"text":"you're","start":2564470,"end":2564670,"confidence":0.9991862,"speaker":"A"},{"text":"probably","start":2564670,"end":2564870,"confidence":1,"speaker":"A"},{"text":"wondering","start":2564870,"end":2565270,"confidence":0.99121094,"speaker":"A"},{"text":"where","start":2565270,"end":2565510,"confidence":0.9995117,"speaker":"A"},{"text":"is","start":2565510,"end":2565670,"confidence":0.88183594,"speaker":"A"},{"text":"all","start":2565670,"end":2565830,"confidence":0.99121094,"speaker":"A"},{"text":"these.","start":2565830,"end":2566110,"confidence":0.8798828,"speaker":"A"},{"text":"The","start":2566110,"end":2566390,"confidence":0.8417969,"speaker":"A"},{"text":"stuff","start":2566390,"end":2566710,"confidence":0.99853516,"speaker":"A"},{"text":"all","start":2566710,"end":2566950,"confidence":0.9892578,"speaker":"A"},{"text":"these","start":2566950,"end":2567110,"confidence":0.7866211,"speaker":"A"},{"text":"secrets","start":2567110,"end":2567510,"confidence":0.97875977,"speaker":"A"},{"text":"stored?","start":2567510,"end":2567870,"confidence":0.98657227,"speaker":"A"},{"text":"Yes,","start":2567870,"end":2568150,"confidence":0.99975586,"speaker":"A"},{"text":"they","start":2568150,"end":2568310,"confidence":0.99902344,"speaker":"A"},{"text":"are","start":2568310,"end":2568510,"confidence":0.99902344,"speaker":"A"},{"text":"stored","start":2568510,"end":2568990,"confidence":0.99731445,"speaker":"A"},{"text":"in","start":2569790,"end":2570150,"confidence":0.9765625,"speaker":"A"},{"text":"Actions","start":2570150,"end":2570830,"confidence":0.9909668,"speaker":"A"},{"text":"secrets","start":2570990,"end":2571790,"confidence":0.998291,"speaker":"A"},{"text":"right","start":2572430,"end":2572750,"confidence":0.99853516,"speaker":"A"},{"text":"here.","start":2572750,"end":2573070,"confidence":0.9995117,"speaker":"A"}]},{"text":"So we have our private key ID API key from Virtual Buddy. So that's all stored there. Here is Celestra. It's for updating RSS feeds. So it just basically goes through.","start":2573310,"end":2588490,"confidence":0.94384766,"words":[{"text":"So","start":2573310,"end":2573589,"confidence":0.94384766,"speaker":"A"},{"text":"we","start":2573589,"end":2573750,"confidence":1,"speaker":"A"},{"text":"have","start":2573750,"end":2573910,"confidence":1,"speaker":"A"},{"text":"our","start":2573910,"end":2574070,"confidence":0.8671875,"speaker":"A"},{"text":"private","start":2574070,"end":2574310,"confidence":0.9995117,"speaker":"A"},{"text":"key","start":2574310,"end":2574670,"confidence":0.9980469,"speaker":"A"},{"text":"ID","start":2575310,"end":2575710,"confidence":0.8774414,"speaker":"A"},{"text":"API","start":2576510,"end":2577070,"confidence":0.98535156,"speaker":"A"},{"text":"key","start":2577070,"end":2577390,"confidence":0.9970703,"speaker":"A"},{"text":"from","start":2577790,"end":2578190,"confidence":0.9995117,"speaker":"A"},{"text":"Virtual","start":2578190,"end":2578670,"confidence":0.99975586,"speaker":"A"},{"text":"Buddy.","start":2578670,"end":2579150,"confidence":0.97786456,"speaker":"A"},{"text":"So","start":2579550,"end":2579950,"confidence":0.9667969,"speaker":"A"},{"text":"that's","start":2580030,"end":2580430,"confidence":0.99625653,"speaker":"A"},{"text":"all","start":2580430,"end":2580550,"confidence":0.98779297,"speaker":"A"},{"text":"stored","start":2580550,"end":2580950,"confidence":0.9921875,"speaker":"A"},{"text":"there.","start":2580950,"end":2581230,"confidence":0.99658203,"speaker":"A"},{"text":"Here","start":2581870,"end":2582270,"confidence":0.99853516,"speaker":"A"},{"text":"is","start":2582350,"end":2582750,"confidence":0.9975586,"speaker":"A"},{"text":"Celestra.","start":2583150,"end":2583950,"confidence":0.8902995,"speaker":"A"},{"text":"It's","start":2584270,"end":2584710,"confidence":0.99886066,"speaker":"A"},{"text":"for","start":2584710,"end":2584910,"confidence":0.99902344,"speaker":"A"},{"text":"updating","start":2584910,"end":2585350,"confidence":0.9995117,"speaker":"A"},{"text":"RSS","start":2585350,"end":2585830,"confidence":0.9616699,"speaker":"A"},{"text":"feeds.","start":2585830,"end":2586350,"confidence":0.9967448,"speaker":"A"},{"text":"So","start":2587050,"end":2587130,"confidence":0.97216797,"speaker":"A"},{"text":"it","start":2587130,"end":2587210,"confidence":0.9663086,"speaker":"A"},{"text":"just","start":2587210,"end":2587370,"confidence":0.9951172,"speaker":"A"},{"text":"basically","start":2587370,"end":2587810,"confidence":0.99975586,"speaker":"A"},{"text":"goes","start":2587810,"end":2588170,"confidence":0.9995117,"speaker":"A"},{"text":"through.","start":2588170,"end":2588490,"confidence":0.9995117,"speaker":"A"}]},{"text":"You can look at the Swift code it goes through, pulls RSS feeds and updates them into a CloudKit record or what do you call it? Yeah, record type. And I of course try to do it in such a way not to hammer people, but same idea, yeah, it goes ahead and it runs the binary it updates and then I also have like actual parameters that I take to to filter out, like which RSS feeds are high priority and which ones aren't based on the audience and etc. So yeah, so that's deployment. That's how you can get that working.","start":2588570,"end":2628410,"confidence":0.9995117,"words":[{"text":"You","start":2588570,"end":2588810,"confidence":0.9995117,"speaker":"A"},{"text":"can","start":2588810,"end":2588930,"confidence":0.9995117,"speaker":"A"},{"text":"look","start":2588930,"end":2589090,"confidence":1,"speaker":"A"},{"text":"at","start":2589090,"end":2589210,"confidence":1,"speaker":"A"},{"text":"the","start":2589210,"end":2589290,"confidence":0.9951172,"speaker":"A"},{"text":"Swift","start":2589290,"end":2589610,"confidence":0.99902344,"speaker":"A"},{"text":"code","start":2589610,"end":2589930,"confidence":0.976888,"speaker":"A"},{"text":"it","start":2589930,"end":2590130,"confidence":0.9995117,"speaker":"A"},{"text":"goes","start":2590130,"end":2590370,"confidence":0.9995117,"speaker":"A"},{"text":"through,","start":2590370,"end":2590610,"confidence":0.9995117,"speaker":"A"},{"text":"pulls","start":2590610,"end":2590970,"confidence":0.97249347,"speaker":"A"},{"text":"RSS","start":2590970,"end":2591370,"confidence":0.98217773,"speaker":"A"},{"text":"feeds","start":2591370,"end":2591890,"confidence":0.9975586,"speaker":"A"},{"text":"and","start":2591890,"end":2592090,"confidence":0.9975586,"speaker":"A"},{"text":"updates","start":2592090,"end":2592650,"confidence":0.9995117,"speaker":"A"},{"text":"them","start":2593050,"end":2593370,"confidence":0.98876953,"speaker":"A"},{"text":"into","start":2593370,"end":2593650,"confidence":0.9995117,"speaker":"A"},{"text":"a","start":2593650,"end":2593850,"confidence":0.9970703,"speaker":"A"},{"text":"CloudKit","start":2593850,"end":2594490,"confidence":0.9980469,"speaker":"A"},{"text":"record","start":2595530,"end":2595930,"confidence":0.99902344,"speaker":"A"},{"text":"or","start":2596410,"end":2596810,"confidence":0.9975586,"speaker":"A"},{"text":"what","start":2596890,"end":2597130,"confidence":0.9321289,"speaker":"A"},{"text":"do","start":2597130,"end":2597210,"confidence":0.8364258,"speaker":"A"},{"text":"you","start":2597210,"end":2597290,"confidence":0.9980469,"speaker":"A"},{"text":"call","start":2597290,"end":2597370,"confidence":1,"speaker":"A"},{"text":"it?","start":2597370,"end":2597490,"confidence":0.9951172,"speaker":"A"},{"text":"Yeah,","start":2597490,"end":2597730,"confidence":0.9558919,"speaker":"A"},{"text":"record","start":2597730,"end":2598010,"confidence":0.99853516,"speaker":"A"},{"text":"type.","start":2598010,"end":2598490,"confidence":0.9250488,"speaker":"A"},{"text":"And","start":2599850,"end":2600130,"confidence":0.9638672,"speaker":"A"},{"text":"I","start":2600130,"end":2600290,"confidence":0.9946289,"speaker":"A"},{"text":"of","start":2600290,"end":2600410,"confidence":0.64501953,"speaker":"A"},{"text":"course","start":2600410,"end":2600570,"confidence":0.9995117,"speaker":"A"},{"text":"try","start":2600570,"end":2600770,"confidence":0.9506836,"speaker":"A"},{"text":"to","start":2600770,"end":2600890,"confidence":0.9995117,"speaker":"A"},{"text":"do","start":2600890,"end":2600970,"confidence":1,"speaker":"A"},{"text":"it","start":2600970,"end":2601050,"confidence":0.9980469,"speaker":"A"},{"text":"in","start":2601050,"end":2601130,"confidence":0.98876953,"speaker":"A"},{"text":"such","start":2601130,"end":2601250,"confidence":1,"speaker":"A"},{"text":"a","start":2601250,"end":2601370,"confidence":0.96777344,"speaker":"A"},{"text":"way","start":2601370,"end":2601530,"confidence":1,"speaker":"A"},{"text":"not","start":2601530,"end":2601730,"confidence":0.99365234,"speaker":"A"},{"text":"to","start":2601730,"end":2601890,"confidence":0.9980469,"speaker":"A"},{"text":"hammer","start":2601890,"end":2602210,"confidence":0.9998372,"speaker":"A"},{"text":"people,","start":2602210,"end":2602490,"confidence":0.9995117,"speaker":"A"},{"text":"but","start":2602970,"end":2603370,"confidence":0.9902344,"speaker":"A"},{"text":"same","start":2603370,"end":2603690,"confidence":0.9941406,"speaker":"A"},{"text":"idea,","start":2603690,"end":2604170,"confidence":0.9914551,"speaker":"A"},{"text":"yeah,","start":2607050,"end":2607410,"confidence":0.96761066,"speaker":"A"},{"text":"it","start":2607410,"end":2607570,"confidence":0.99902344,"speaker":"A"},{"text":"goes","start":2607570,"end":2607770,"confidence":1,"speaker":"A"},{"text":"ahead","start":2607770,"end":2608010,"confidence":0.99853516,"speaker":"A"},{"text":"and","start":2608010,"end":2608330,"confidence":0.9921875,"speaker":"A"},{"text":"it","start":2608330,"end":2608570,"confidence":0.98828125,"speaker":"A"},{"text":"runs","start":2608570,"end":2609130,"confidence":0.9995117,"speaker":"A"},{"text":"the","start":2610330,"end":2610610,"confidence":0.9995117,"speaker":"A"},{"text":"binary","start":2610610,"end":2611210,"confidence":0.9991862,"speaker":"A"},{"text":"it","start":2611210,"end":2611530,"confidence":0.9711914,"speaker":"A"},{"text":"updates","start":2611530,"end":2612010,"confidence":0.9992676,"speaker":"A"},{"text":"and","start":2612170,"end":2612410,"confidence":0.98828125,"speaker":"A"},{"text":"then","start":2612410,"end":2612570,"confidence":0.99902344,"speaker":"A"},{"text":"I","start":2612570,"end":2612770,"confidence":0.9995117,"speaker":"A"},{"text":"also","start":2612770,"end":2612970,"confidence":0.9995117,"speaker":"A"},{"text":"have","start":2612970,"end":2613290,"confidence":0.99902344,"speaker":"A"},{"text":"like","start":2613290,"end":2613650,"confidence":0.9321289,"speaker":"A"},{"text":"actual","start":2613650,"end":2614170,"confidence":0.99853516,"speaker":"A"},{"text":"parameters","start":2615370,"end":2615890,"confidence":0.99902344,"speaker":"A"},{"text":"that","start":2615890,"end":2616010,"confidence":0.99902344,"speaker":"A"},{"text":"I","start":2616010,"end":2616130,"confidence":0.9995117,"speaker":"A"},{"text":"take","start":2616130,"end":2616330,"confidence":1,"speaker":"A"},{"text":"to","start":2616330,"end":2616570,"confidence":0.97314453,"speaker":"A"},{"text":"to","start":2616570,"end":2616810,"confidence":0.9995117,"speaker":"A"},{"text":"filter","start":2616810,"end":2617170,"confidence":0.9663086,"speaker":"A"},{"text":"out,","start":2617170,"end":2617410,"confidence":1,"speaker":"A"},{"text":"like","start":2617410,"end":2617610,"confidence":0.99658203,"speaker":"A"},{"text":"which","start":2617610,"end":2617890,"confidence":0.99902344,"speaker":"A"},{"text":"RSS","start":2617890,"end":2618410,"confidence":0.99853516,"speaker":"A"},{"text":"feeds","start":2618410,"end":2618970,"confidence":0.9991862,"speaker":"A"},{"text":"are","start":2619290,"end":2619610,"confidence":0.96240234,"speaker":"A"},{"text":"high","start":2619610,"end":2619810,"confidence":1,"speaker":"A"},{"text":"priority","start":2619810,"end":2620170,"confidence":1,"speaker":"A"},{"text":"and","start":2620170,"end":2620330,"confidence":0.92626953,"speaker":"A"},{"text":"which","start":2620330,"end":2620450,"confidence":1,"speaker":"A"},{"text":"ones","start":2620450,"end":2620690,"confidence":0.9995117,"speaker":"A"},{"text":"aren't","start":2620690,"end":2621010,"confidence":0.99768066,"speaker":"A"},{"text":"based","start":2621010,"end":2621170,"confidence":1,"speaker":"A"},{"text":"on","start":2621170,"end":2621330,"confidence":1,"speaker":"A"},{"text":"the","start":2621330,"end":2621490,"confidence":0.99365234,"speaker":"A"},{"text":"audience","start":2621490,"end":2621770,"confidence":0.99853516,"speaker":"A"},{"text":"and","start":2621770,"end":2621970,"confidence":0.9975586,"speaker":"A"},{"text":"etc.","start":2621970,"end":2622650,"confidence":0.90723,"speaker":"A"},{"text":"So","start":2622650,"end":2623050,"confidence":0.9946289,"speaker":"A"},{"text":"yeah,","start":2623850,"end":2624330,"confidence":0.95377606,"speaker":"A"},{"text":"so","start":2624890,"end":2625170,"confidence":0.99853516,"speaker":"A"},{"text":"that's","start":2625170,"end":2625450,"confidence":0.9946289,"speaker":"A"},{"text":"deployment.","start":2625450,"end":2626170,"confidence":0.9991862,"speaker":"A"},{"text":"That's","start":2627050,"end":2627450,"confidence":0.9998372,"speaker":"A"},{"text":"how","start":2627450,"end":2627530,"confidence":1,"speaker":"A"},{"text":"you","start":2627530,"end":2627650,"confidence":0.9995117,"speaker":"A"},{"text":"can","start":2627650,"end":2627770,"confidence":1,"speaker":"A"},{"text":"get","start":2627770,"end":2627890,"confidence":1,"speaker":"A"},{"text":"that","start":2627890,"end":2628090,"confidence":1,"speaker":"A"},{"text":"working.","start":2628090,"end":2628410,"confidence":0.9995117,"speaker":"A"}]},{"text":"There's weird stuff with cloud with GitHub that I've noticed. If you haven't updated it in a while, it doesn't run these cron jobs. So I need to figure out a how to get around it or find another service to do it. This is all free because it's public and it is running on Ubuntu. So that's really great.","start":2628810,"end":2649870,"confidence":0.9996745,"words":[{"text":"There's","start":2628810,"end":2629250,"confidence":0.9996745,"speaker":"A"},{"text":"weird","start":2629250,"end":2629490,"confidence":1,"speaker":"A"},{"text":"stuff","start":2629490,"end":2629690,"confidence":1,"speaker":"A"},{"text":"with","start":2629690,"end":2629850,"confidence":0.99609375,"speaker":"A"},{"text":"cloud","start":2629850,"end":2630290,"confidence":0.8815918,"speaker":"A"},{"text":"with","start":2630290,"end":2630650,"confidence":0.9873047,"speaker":"A"},{"text":"GitHub","start":2630810,"end":2631530,"confidence":0.99853516,"speaker":"A"},{"text":"that","start":2632730,"end":2633130,"confidence":0.9975586,"speaker":"A"},{"text":"I've","start":2633690,"end":2634010,"confidence":1,"speaker":"A"},{"text":"noticed.","start":2634010,"end":2634330,"confidence":0.99869794,"speaker":"A"},{"text":"If","start":2634330,"end":2634530,"confidence":0.9975586,"speaker":"A"},{"text":"you","start":2634530,"end":2634730,"confidence":0.9995117,"speaker":"A"},{"text":"haven't","start":2634730,"end":2635010,"confidence":0.9984131,"speaker":"A"},{"text":"updated","start":2635010,"end":2635370,"confidence":0.9995117,"speaker":"A"},{"text":"it","start":2635370,"end":2635610,"confidence":0.96240234,"speaker":"A"},{"text":"in","start":2635610,"end":2635810,"confidence":0.99902344,"speaker":"A"},{"text":"a","start":2635810,"end":2635970,"confidence":0.99560547,"speaker":"A"},{"text":"while,","start":2635970,"end":2636250,"confidence":0.9995117,"speaker":"A"},{"text":"it","start":2636250,"end":2636530,"confidence":1,"speaker":"A"},{"text":"doesn't","start":2636530,"end":2636770,"confidence":0.9998372,"speaker":"A"},{"text":"run","start":2636770,"end":2636970,"confidence":0.99853516,"speaker":"A"},{"text":"these","start":2636970,"end":2637210,"confidence":0.96777344,"speaker":"A"},{"text":"cron","start":2637210,"end":2637490,"confidence":0.90527344,"speaker":"A"},{"text":"jobs.","start":2637490,"end":2637770,"confidence":0.99072266,"speaker":"A"},{"text":"So","start":2637770,"end":2637850,"confidence":0.9951172,"speaker":"A"},{"text":"I","start":2637850,"end":2637930,"confidence":1,"speaker":"A"},{"text":"need","start":2637930,"end":2638050,"confidence":1,"speaker":"A"},{"text":"to","start":2638050,"end":2638170,"confidence":0.99902344,"speaker":"A"},{"text":"figure","start":2638170,"end":2638330,"confidence":0.99975586,"speaker":"A"},{"text":"out","start":2638330,"end":2638490,"confidence":0.98828125,"speaker":"A"},{"text":"a","start":2638490,"end":2638690,"confidence":0.89941406,"speaker":"A"},{"text":"how","start":2638690,"end":2638850,"confidence":0.99853516,"speaker":"A"},{"text":"to","start":2638850,"end":2638970,"confidence":0.9995117,"speaker":"A"},{"text":"get","start":2638970,"end":2639050,"confidence":0.9995117,"speaker":"A"},{"text":"around","start":2639050,"end":2639210,"confidence":0.99853516,"speaker":"A"},{"text":"it","start":2639210,"end":2639410,"confidence":0.9238281,"speaker":"A"},{"text":"or","start":2639410,"end":2639570,"confidence":0.9995117,"speaker":"A"},{"text":"find","start":2639570,"end":2639730,"confidence":0.9995117,"speaker":"A"},{"text":"another","start":2639730,"end":2640010,"confidence":0.9477539,"speaker":"A"},{"text":"service","start":2640090,"end":2640450,"confidence":0.9819336,"speaker":"A"},{"text":"to","start":2640450,"end":2640650,"confidence":0.9970703,"speaker":"A"},{"text":"do","start":2640650,"end":2640730,"confidence":0.99902344,"speaker":"A"},{"text":"it.","start":2640730,"end":2640970,"confidence":0.9975586,"speaker":"A"},{"text":"This","start":2642830,"end":2642950,"confidence":0.9897461,"speaker":"A"},{"text":"is","start":2642950,"end":2643110,"confidence":0.9975586,"speaker":"A"},{"text":"all","start":2643110,"end":2643270,"confidence":0.9995117,"speaker":"A"},{"text":"free","start":2643270,"end":2643550,"confidence":1,"speaker":"A"},{"text":"because","start":2643630,"end":2644030,"confidence":0.9995117,"speaker":"A"},{"text":"it's","start":2644110,"end":2644590,"confidence":0.99934894,"speaker":"A"},{"text":"public","start":2644590,"end":2644870,"confidence":1,"speaker":"A"},{"text":"and","start":2644870,"end":2645230,"confidence":0.7548828,"speaker":"A"},{"text":"it","start":2646990,"end":2647310,"confidence":0.99902344,"speaker":"A"},{"text":"is","start":2647310,"end":2647550,"confidence":0.9995117,"speaker":"A"},{"text":"running","start":2647550,"end":2647870,"confidence":0.9987793,"speaker":"A"},{"text":"on","start":2647870,"end":2647990,"confidence":0.7963867,"speaker":"A"},{"text":"Ubuntu.","start":2647990,"end":2648590,"confidence":0.8631836,"speaker":"A"},{"text":"So","start":2648670,"end":2648910,"confidence":0.9980469,"speaker":"A"},{"text":"that's","start":2648910,"end":2649310,"confidence":0.99934894,"speaker":"A"},{"text":"really","start":2649310,"end":2649550,"confidence":1,"speaker":"A"},{"text":"great.","start":2649550,"end":2649870,"confidence":0.99902344,"speaker":"A"}]},{"text":"And the storage on CloudKit is dirt cheap, which is even more awesome.","start":2652350,"end":2656830,"confidence":0.9838867,"words":[{"text":"And","start":2652350,"end":2652750,"confidence":0.9838867,"speaker":"A"},{"text":"the","start":2652830,"end":2653110,"confidence":0.9995117,"speaker":"A"},{"text":"storage","start":2653110,"end":2653430,"confidence":1,"speaker":"A"},{"text":"on","start":2653430,"end":2653590,"confidence":0.9951172,"speaker":"A"},{"text":"CloudKit","start":2653590,"end":2654150,"confidence":0.94189453,"speaker":"A"},{"text":"is","start":2654150,"end":2654310,"confidence":0.99902344,"speaker":"A"},{"text":"dirt","start":2654310,"end":2654590,"confidence":0.8517253,"speaker":"A"},{"text":"cheap,","start":2654590,"end":2654990,"confidence":0.8378906,"speaker":"A"},{"text":"which","start":2655390,"end":2655670,"confidence":0.99902344,"speaker":"A"},{"text":"is","start":2655670,"end":2655830,"confidence":1,"speaker":"A"},{"text":"even","start":2655830,"end":2656070,"confidence":1,"speaker":"A"},{"text":"more","start":2656070,"end":2656310,"confidence":1,"speaker":"A"},{"text":"awesome.","start":2656310,"end":2656830,"confidence":0.99886066,"speaker":"A"}]},{"text":"Sorry, let's see what else. I just want to make sure I covered all my slides. The last thing I'm going to talk about is just what are my plans? Excuse me. So I don't know if you check.","start":2660030,"end":2672790,"confidence":0.99593097,"words":[{"text":"Sorry,","start":2660030,"end":2660590,"confidence":0.99593097,"speaker":"A"},{"text":"let's","start":2660990,"end":2661350,"confidence":0.89501953,"speaker":"A"},{"text":"see","start":2661350,"end":2661550,"confidence":0.9848633,"speaker":"A"},{"text":"what","start":2661550,"end":2661750,"confidence":0.99609375,"speaker":"A"},{"text":"else.","start":2661750,"end":2662110,"confidence":0.99975586,"speaker":"A"},{"text":"I","start":2663630,"end":2663870,"confidence":0.9682617,"speaker":"A"},{"text":"just","start":2663870,"end":2663990,"confidence":0.9824219,"speaker":"A"},{"text":"want","start":2663990,"end":2664110,"confidence":0.75878906,"speaker":"A"},{"text":"to","start":2664110,"end":2664230,"confidence":0.7807617,"speaker":"A"},{"text":"make","start":2664230,"end":2664350,"confidence":0.9995117,"speaker":"A"},{"text":"sure","start":2664350,"end":2664430,"confidence":1,"speaker":"A"},{"text":"I","start":2664430,"end":2664550,"confidence":0.98779297,"speaker":"A"},{"text":"covered","start":2664550,"end":2664870,"confidence":0.99975586,"speaker":"A"},{"text":"all","start":2664870,"end":2665070,"confidence":0.99902344,"speaker":"A"},{"text":"my","start":2665070,"end":2665390,"confidence":0.9970703,"speaker":"A"},{"text":"slides.","start":2665630,"end":2666150,"confidence":0.99975586,"speaker":"A"},{"text":"The","start":2666150,"end":2666390,"confidence":0.9995117,"speaker":"A"},{"text":"last","start":2666390,"end":2666590,"confidence":1,"speaker":"A"},{"text":"thing","start":2666590,"end":2666790,"confidence":1,"speaker":"A"},{"text":"I'm","start":2666790,"end":2666990,"confidence":0.9980469,"speaker":"A"},{"text":"going","start":2666990,"end":2667070,"confidence":0.96777344,"speaker":"A"},{"text":"to","start":2667070,"end":2667150,"confidence":0.9995117,"speaker":"A"},{"text":"talk","start":2667150,"end":2667270,"confidence":1,"speaker":"A"},{"text":"about","start":2667270,"end":2667470,"confidence":0.9995117,"speaker":"A"},{"text":"is","start":2667470,"end":2667670,"confidence":0.9941406,"speaker":"A"},{"text":"just","start":2667670,"end":2667830,"confidence":0.9941406,"speaker":"A"},{"text":"what","start":2667830,"end":2667990,"confidence":0.99853516,"speaker":"A"},{"text":"are","start":2667990,"end":2668150,"confidence":0.99902344,"speaker":"A"},{"text":"my","start":2668150,"end":2668310,"confidence":1,"speaker":"A"},{"text":"plans?","start":2668310,"end":2668670,"confidence":0.92578125,"speaker":"A"},{"text":"Excuse","start":2670390,"end":2670750,"confidence":0.9793294,"speaker":"A"},{"text":"me.","start":2670750,"end":2671030,"confidence":1,"speaker":"A"},{"text":"So","start":2671510,"end":2671790,"confidence":0.9980469,"speaker":"A"},{"text":"I","start":2671790,"end":2671910,"confidence":0.9995117,"speaker":"A"},{"text":"don't","start":2671910,"end":2672070,"confidence":0.99934894,"speaker":"A"},{"text":"know","start":2672070,"end":2672150,"confidence":1,"speaker":"A"},{"text":"if","start":2672150,"end":2672230,"confidence":1,"speaker":"A"},{"text":"you","start":2672230,"end":2672390,"confidence":0.9995117,"speaker":"A"},{"text":"check.","start":2672390,"end":2672790,"confidence":0.7727051,"speaker":"A"}]},{"text":"Follow me. But I just released.","start":2672790,"end":2674550,"confidence":0.9663086,"words":[{"text":"Follow","start":2672790,"end":2673150,"confidence":0.9663086,"speaker":"A"},{"text":"me.","start":2673150,"end":2673390,"confidence":1,"speaker":"A"},{"text":"But","start":2673390,"end":2673550,"confidence":0.99853516,"speaker":"A"},{"text":"I","start":2673550,"end":2673710,"confidence":0.9995117,"speaker":"A"},{"text":"just","start":2673710,"end":2673910,"confidence":0.99902344,"speaker":"A"},{"text":"released.","start":2673910,"end":2674550,"confidence":0.99975586,"speaker":"A"}]},{"text":"I just released Alpha 5 that has lookup zones, fetch, record changes and upload assets. Upload the assets is pretty awesome. When I saw that work because I was like, cool, I can actually upload a binary to CloudKit, which is awesome. We got query filters to work for in and not in, so you could do that I have plans to continue working on this because I think there's a big future for something like this for a lot of people.","start":2681910,"end":2706990,"confidence":0.98876953,"words":[{"text":"I","start":2681910,"end":2682190,"confidence":0.98876953,"speaker":"A"},{"text":"just","start":2682190,"end":2682350,"confidence":1,"speaker":"A"},{"text":"released","start":2682350,"end":2682710,"confidence":0.99975586,"speaker":"A"},{"text":"Alpha","start":2682710,"end":2683150,"confidence":0.85091144,"speaker":"A"},{"text":"5","start":2683150,"end":2683430,"confidence":0.99414,"speaker":"A"},{"text":"that","start":2684310,"end":2684630,"confidence":1,"speaker":"A"},{"text":"has","start":2684630,"end":2684909,"confidence":0.9995117,"speaker":"A"},{"text":"lookup","start":2684909,"end":2685390,"confidence":0.89086914,"speaker":"A"},{"text":"zones,","start":2685390,"end":2685750,"confidence":0.9760742,"speaker":"A"},{"text":"fetch,","start":2685750,"end":2686150,"confidence":0.9900716,"speaker":"A"},{"text":"record","start":2686150,"end":2686430,"confidence":0.9995117,"speaker":"A"},{"text":"changes","start":2686430,"end":2686870,"confidence":0.99975586,"speaker":"A"},{"text":"and","start":2686870,"end":2687030,"confidence":0.6220703,"speaker":"A"},{"text":"upload","start":2687030,"end":2687430,"confidence":0.71809894,"speaker":"A"},{"text":"assets.","start":2687430,"end":2687990,"confidence":1,"speaker":"A"},{"text":"Upload","start":2688310,"end":2688750,"confidence":0.9840495,"speaker":"A"},{"text":"the","start":2688750,"end":2688910,"confidence":0.7114258,"speaker":"A"},{"text":"assets","start":2688910,"end":2689270,"confidence":0.99902344,"speaker":"A"},{"text":"is","start":2689270,"end":2689470,"confidence":0.9814453,"speaker":"A"},{"text":"pretty","start":2689470,"end":2689710,"confidence":1,"speaker":"A"},{"text":"awesome.","start":2689710,"end":2690150,"confidence":1,"speaker":"A"},{"text":"When","start":2690230,"end":2690510,"confidence":0.99902344,"speaker":"A"},{"text":"I","start":2690510,"end":2690670,"confidence":1,"speaker":"A"},{"text":"saw","start":2690670,"end":2690830,"confidence":1,"speaker":"A"},{"text":"that","start":2690830,"end":2691030,"confidence":0.9995117,"speaker":"A"},{"text":"work","start":2691030,"end":2691310,"confidence":0.99902344,"speaker":"A"},{"text":"because","start":2691310,"end":2691590,"confidence":1,"speaker":"A"},{"text":"I","start":2691590,"end":2691750,"confidence":0.9536133,"speaker":"A"},{"text":"was","start":2691750,"end":2691870,"confidence":0.9975586,"speaker":"A"},{"text":"like,","start":2691870,"end":2691990,"confidence":0.9980469,"speaker":"A"},{"text":"cool,","start":2691990,"end":2692190,"confidence":0.99853516,"speaker":"A"},{"text":"I","start":2692190,"end":2692310,"confidence":0.9951172,"speaker":"A"},{"text":"can","start":2692310,"end":2692470,"confidence":0.9970703,"speaker":"A"},{"text":"actually","start":2692470,"end":2692670,"confidence":0.9995117,"speaker":"A"},{"text":"upload","start":2692670,"end":2693030,"confidence":1,"speaker":"A"},{"text":"a","start":2693030,"end":2693150,"confidence":0.9951172,"speaker":"A"},{"text":"binary","start":2693150,"end":2693750,"confidence":0.99853516,"speaker":"A"},{"text":"to","start":2694630,"end":2694910,"confidence":0.96728516,"speaker":"A"},{"text":"CloudKit,","start":2694910,"end":2695510,"confidence":0.98046875,"speaker":"A"},{"text":"which","start":2695510,"end":2695710,"confidence":0.9995117,"speaker":"A"},{"text":"is","start":2695710,"end":2695830,"confidence":0.9995117,"speaker":"A"},{"text":"awesome.","start":2695830,"end":2696230,"confidence":0.9998372,"speaker":"A"},{"text":"We","start":2697310,"end":2697430,"confidence":0.99121094,"speaker":"A"},{"text":"got","start":2697430,"end":2697630,"confidence":0.9946289,"speaker":"A"},{"text":"query","start":2697630,"end":2697990,"confidence":0.9836426,"speaker":"A"},{"text":"filters","start":2697990,"end":2698470,"confidence":0.9889323,"speaker":"A"},{"text":"to","start":2698470,"end":2698630,"confidence":0.99853516,"speaker":"A"},{"text":"work","start":2698630,"end":2698790,"confidence":1,"speaker":"A"},{"text":"for","start":2698790,"end":2698950,"confidence":0.9980469,"speaker":"A"},{"text":"in","start":2698950,"end":2699150,"confidence":0.88183594,"speaker":"A"},{"text":"and","start":2699150,"end":2699310,"confidence":0.9741211,"speaker":"A"},{"text":"not","start":2699310,"end":2699510,"confidence":0.98339844,"speaker":"A"},{"text":"in,","start":2699510,"end":2699870,"confidence":0.8652344,"speaker":"A"},{"text":"so","start":2699870,"end":2700110,"confidence":0.99853516,"speaker":"A"},{"text":"you","start":2700110,"end":2700190,"confidence":0.99853516,"speaker":"A"},{"text":"could","start":2700190,"end":2700350,"confidence":0.95410156,"speaker":"A"},{"text":"do","start":2700350,"end":2700550,"confidence":1,"speaker":"A"},{"text":"that","start":2700550,"end":2700830,"confidence":0.9995117,"speaker":"A"},{"text":"I","start":2701470,"end":2701790,"confidence":0.99902344,"speaker":"A"},{"text":"have","start":2701790,"end":2702110,"confidence":0.9995117,"speaker":"A"},{"text":"plans","start":2702110,"end":2702630,"confidence":0.95043945,"speaker":"A"},{"text":"to","start":2702630,"end":2702750,"confidence":0.95166016,"speaker":"A"},{"text":"continue","start":2702750,"end":2702950,"confidence":0.9980469,"speaker":"A"},{"text":"working","start":2702950,"end":2703230,"confidence":0.9238281,"speaker":"A"},{"text":"on","start":2703230,"end":2703430,"confidence":0.99853516,"speaker":"A"},{"text":"this","start":2703430,"end":2703630,"confidence":0.99902344,"speaker":"A"},{"text":"because","start":2703630,"end":2703830,"confidence":0.9555664,"speaker":"A"},{"text":"I","start":2703830,"end":2703990,"confidence":0.9995117,"speaker":"A"},{"text":"think","start":2703990,"end":2704230,"confidence":0.99902344,"speaker":"A"},{"text":"there's","start":2704230,"end":2704710,"confidence":0.9991862,"speaker":"A"},{"text":"a","start":2704710,"end":2704830,"confidence":0.9995117,"speaker":"A"},{"text":"big","start":2704830,"end":2704990,"confidence":0.99902344,"speaker":"A"},{"text":"future","start":2704990,"end":2705270,"confidence":0.9970703,"speaker":"A"},{"text":"for","start":2705270,"end":2705510,"confidence":0.9995117,"speaker":"A"},{"text":"something","start":2705510,"end":2705750,"confidence":0.99560547,"speaker":"A"},{"text":"like","start":2705750,"end":2705990,"confidence":0.9995117,"speaker":"A"},{"text":"this","start":2705990,"end":2706190,"confidence":0.9995117,"speaker":"A"},{"text":"for","start":2706190,"end":2706390,"confidence":0.9995117,"speaker":"A"},{"text":"a","start":2706390,"end":2706510,"confidence":0.9995117,"speaker":"A"},{"text":"lot","start":2706510,"end":2706590,"confidence":1,"speaker":"A"},{"text":"of","start":2706590,"end":2706710,"confidence":0.9995117,"speaker":"A"},{"text":"people.","start":2706710,"end":2706990,"confidence":0.9995117,"speaker":"A"}]},{"text":"Yes, you can technically use this in Android or Windows because the Swift thing does compile in Android and Windows. You can see I already added support for that. This is the support I recently had. And then we're. I'm just kind of like going through each of these because as great as AI is, it's not perfect.","start":2709150,"end":2727000,"confidence":0.9716797,"words":[{"text":"Yes,","start":2709150,"end":2709590,"confidence":0.9716797,"speaker":"A"},{"text":"you","start":2709590,"end":2709830,"confidence":0.9995117,"speaker":"A"},{"text":"can","start":2709830,"end":2709990,"confidence":0.93603516,"speaker":"A"},{"text":"technically","start":2709990,"end":2710350,"confidence":0.9992676,"speaker":"A"},{"text":"use","start":2710350,"end":2710590,"confidence":0.9995117,"speaker":"A"},{"text":"this","start":2710590,"end":2710790,"confidence":0.98095703,"speaker":"A"},{"text":"in","start":2710790,"end":2710950,"confidence":0.9633789,"speaker":"A"},{"text":"Android","start":2710950,"end":2711470,"confidence":0.99934894,"speaker":"A"},{"text":"or","start":2711470,"end":2711710,"confidence":0.9995117,"speaker":"A"},{"text":"Windows","start":2711710,"end":2712270,"confidence":0.9972331,"speaker":"A"},{"text":"because","start":2712670,"end":2713070,"confidence":0.9995117,"speaker":"A"},{"text":"the","start":2713230,"end":2713510,"confidence":0.9970703,"speaker":"A"},{"text":"Swift","start":2713510,"end":2713950,"confidence":0.998291,"speaker":"A"},{"text":"thing","start":2714270,"end":2714590,"confidence":0.99902344,"speaker":"A"},{"text":"does","start":2714590,"end":2714830,"confidence":0.9995117,"speaker":"A"},{"text":"compile","start":2714830,"end":2715190,"confidence":0.99487305,"speaker":"A"},{"text":"in","start":2715190,"end":2715350,"confidence":0.78271484,"speaker":"A"},{"text":"Android","start":2715350,"end":2715750,"confidence":0.99853516,"speaker":"A"},{"text":"and","start":2715750,"end":2715910,"confidence":0.72753906,"speaker":"A"},{"text":"Windows.","start":2715910,"end":2716230,"confidence":0.99934894,"speaker":"A"},{"text":"You","start":2716230,"end":2716350,"confidence":0.9970703,"speaker":"A"},{"text":"can","start":2716350,"end":2716430,"confidence":0.88623047,"speaker":"A"},{"text":"see","start":2716430,"end":2716550,"confidence":0.9995117,"speaker":"A"},{"text":"I","start":2716550,"end":2716670,"confidence":0.63378906,"speaker":"A"},{"text":"already","start":2716670,"end":2716830,"confidence":0.99560547,"speaker":"A"},{"text":"added","start":2716830,"end":2717110,"confidence":0.9819336,"speaker":"A"},{"text":"support","start":2717110,"end":2717430,"confidence":1,"speaker":"A"},{"text":"for","start":2717430,"end":2717670,"confidence":1,"speaker":"A"},{"text":"that.","start":2717670,"end":2717950,"confidence":0.9995117,"speaker":"A"},{"text":"This","start":2718430,"end":2718710,"confidence":0.99609375,"speaker":"A"},{"text":"is","start":2718710,"end":2718870,"confidence":0.9975586,"speaker":"A"},{"text":"the","start":2718870,"end":2719030,"confidence":0.88720703,"speaker":"A"},{"text":"support","start":2719030,"end":2719270,"confidence":0.9970703,"speaker":"A"},{"text":"I","start":2719270,"end":2719510,"confidence":0.99658203,"speaker":"A"},{"text":"recently","start":2719510,"end":2719790,"confidence":1,"speaker":"A"},{"text":"had.","start":2719870,"end":2720270,"confidence":0.9995117,"speaker":"A"},{"text":"And","start":2720750,"end":2721030,"confidence":0.9814453,"speaker":"A"},{"text":"then","start":2721030,"end":2721310,"confidence":0.99121094,"speaker":"A"},{"text":"we're.","start":2722120,"end":2722360,"confidence":0.77229816,"speaker":"A"},{"text":"I'm","start":2722360,"end":2722600,"confidence":0.9868164,"speaker":"A"},{"text":"just","start":2722600,"end":2722720,"confidence":0.9995117,"speaker":"A"},{"text":"kind","start":2722720,"end":2722840,"confidence":0.9946289,"speaker":"A"},{"text":"of","start":2722840,"end":2722960,"confidence":0.9370117,"speaker":"A"},{"text":"like","start":2722960,"end":2723200,"confidence":0.99609375,"speaker":"A"},{"text":"going","start":2723200,"end":2723480,"confidence":0.99902344,"speaker":"A"},{"text":"through","start":2723480,"end":2723720,"confidence":1,"speaker":"A"},{"text":"each","start":2723720,"end":2723920,"confidence":0.9995117,"speaker":"A"},{"text":"of","start":2723920,"end":2724040,"confidence":0.9995117,"speaker":"A"},{"text":"these","start":2724040,"end":2724280,"confidence":0.99902344,"speaker":"A"},{"text":"because","start":2724280,"end":2724680,"confidence":0.7866211,"speaker":"A"},{"text":"as","start":2724680,"end":2725000,"confidence":1,"speaker":"A"},{"text":"great","start":2725000,"end":2725240,"confidence":0.9951172,"speaker":"A"},{"text":"as","start":2725240,"end":2725480,"confidence":0.9946289,"speaker":"A"},{"text":"AI","start":2725480,"end":2725880,"confidence":0.8781738,"speaker":"A"},{"text":"is,","start":2725880,"end":2726160,"confidence":0.9946289,"speaker":"A"},{"text":"it's","start":2726160,"end":2726440,"confidence":0.9995117,"speaker":"A"},{"text":"not","start":2726440,"end":2726600,"confidence":0.9995117,"speaker":"A"},{"text":"perfect.","start":2726600,"end":2727000,"confidence":0.9840495,"speaker":"A"}]},{"text":"So we're just kind of going through these piece by piece with each version and hammering these away and then this is actually done. I don't even know why that's there. But yeah, I think system field integration might already be there and there's a few other things. Eventually I'd like to add support. So there, there's a whole API for CloudKit schema management that I could.","start":2727080,"end":2753200,"confidence":0.99853516,"words":[{"text":"So","start":2727080,"end":2727480,"confidence":0.99853516,"speaker":"A"},{"text":"we're","start":2728040,"end":2728360,"confidence":0.99934894,"speaker":"A"},{"text":"just","start":2728360,"end":2728440,"confidence":1,"speaker":"A"},{"text":"kind","start":2728440,"end":2728560,"confidence":0.99365234,"speaker":"A"},{"text":"of","start":2728560,"end":2728680,"confidence":0.98828125,"speaker":"A"},{"text":"going","start":2728680,"end":2728880,"confidence":0.99365234,"speaker":"A"},{"text":"through","start":2728880,"end":2729120,"confidence":1,"speaker":"A"},{"text":"these","start":2729120,"end":2729400,"confidence":0.98779297,"speaker":"A"},{"text":"piece","start":2729720,"end":2730120,"confidence":0.9848633,"speaker":"A"},{"text":"by","start":2730120,"end":2730360,"confidence":0.99902344,"speaker":"A"},{"text":"piece","start":2730360,"end":2730760,"confidence":0.9983724,"speaker":"A"},{"text":"with","start":2730840,"end":2731120,"confidence":0.9995117,"speaker":"A"},{"text":"each","start":2731120,"end":2731400,"confidence":0.9995117,"speaker":"A"},{"text":"version","start":2731640,"end":2732080,"confidence":0.99975586,"speaker":"A"},{"text":"and","start":2732080,"end":2732240,"confidence":0.5917969,"speaker":"A"},{"text":"hammering","start":2732240,"end":2732560,"confidence":0.9977214,"speaker":"A"},{"text":"these","start":2732560,"end":2732760,"confidence":0.99609375,"speaker":"A"},{"text":"away","start":2732760,"end":2733080,"confidence":0.9980469,"speaker":"A"},{"text":"and","start":2735400,"end":2735720,"confidence":0.9951172,"speaker":"A"},{"text":"then","start":2735720,"end":2736040,"confidence":0.9975586,"speaker":"A"},{"text":"this","start":2736680,"end":2736960,"confidence":0.9980469,"speaker":"A"},{"text":"is","start":2736960,"end":2737120,"confidence":0.99365234,"speaker":"A"},{"text":"actually","start":2737120,"end":2737360,"confidence":0.9995117,"speaker":"A"},{"text":"done.","start":2737360,"end":2737640,"confidence":0.9980469,"speaker":"A"},{"text":"I","start":2737640,"end":2737840,"confidence":0.9995117,"speaker":"A"},{"text":"don't","start":2737840,"end":2738000,"confidence":0.98844403,"speaker":"A"},{"text":"even","start":2738000,"end":2738159,"confidence":0.99902344,"speaker":"A"},{"text":"know","start":2738159,"end":2738279,"confidence":1,"speaker":"A"},{"text":"why","start":2738279,"end":2738400,"confidence":0.99902344,"speaker":"A"},{"text":"that's","start":2738400,"end":2738680,"confidence":0.9995117,"speaker":"A"},{"text":"there.","start":2738680,"end":2738880,"confidence":0.99853516,"speaker":"A"},{"text":"But","start":2738880,"end":2739240,"confidence":0.99658203,"speaker":"A"},{"text":"yeah,","start":2739640,"end":2740160,"confidence":0.99934894,"speaker":"A"},{"text":"I","start":2740160,"end":2740400,"confidence":0.83203125,"speaker":"A"},{"text":"think","start":2740400,"end":2740680,"confidence":0.92529297,"speaker":"A"},{"text":"system","start":2740680,"end":2741080,"confidence":0.9995117,"speaker":"A"},{"text":"field","start":2741080,"end":2741480,"confidence":0.9916992,"speaker":"A"},{"text":"integration","start":2741640,"end":2742280,"confidence":0.93859863,"speaker":"A"},{"text":"might","start":2742280,"end":2742480,"confidence":0.9980469,"speaker":"A"},{"text":"already","start":2742480,"end":2742720,"confidence":0.9995117,"speaker":"A"},{"text":"be","start":2742720,"end":2742960,"confidence":1,"speaker":"A"},{"text":"there","start":2742960,"end":2743240,"confidence":1,"speaker":"A"},{"text":"and","start":2743400,"end":2743680,"confidence":0.9980469,"speaker":"A"},{"text":"there's","start":2743680,"end":2743960,"confidence":0.99853516,"speaker":"A"},{"text":"a","start":2743960,"end":2744040,"confidence":0.9995117,"speaker":"A"},{"text":"few","start":2744040,"end":2744160,"confidence":0.9995117,"speaker":"A"},{"text":"other","start":2744160,"end":2744400,"confidence":1,"speaker":"A"},{"text":"things.","start":2744400,"end":2744760,"confidence":0.9995117,"speaker":"A"},{"text":"Eventually","start":2745960,"end":2746520,"confidence":0.9992676,"speaker":"A"},{"text":"I'd","start":2746520,"end":2746800,"confidence":0.92122394,"speaker":"A"},{"text":"like","start":2746800,"end":2746960,"confidence":0.9995117,"speaker":"A"},{"text":"to","start":2746960,"end":2747160,"confidence":0.99902344,"speaker":"A"},{"text":"add","start":2747160,"end":2747480,"confidence":0.9975586,"speaker":"A"},{"text":"support.","start":2747880,"end":2748120,"confidence":0.9902344,"speaker":"A"},{"text":"So","start":2748200,"end":2748480,"confidence":0.99902344,"speaker":"A"},{"text":"there,","start":2748480,"end":2748720,"confidence":0.38134766,"speaker":"A"},{"text":"there's","start":2748720,"end":2749080,"confidence":0.9998372,"speaker":"A"},{"text":"a","start":2749080,"end":2749200,"confidence":0.9995117,"speaker":"A"},{"text":"whole","start":2749200,"end":2749440,"confidence":0.99975586,"speaker":"A"},{"text":"API","start":2749440,"end":2749880,"confidence":0.9975586,"speaker":"A"},{"text":"for","start":2749880,"end":2750120,"confidence":0.9975586,"speaker":"A"},{"text":"CloudKit","start":2750120,"end":2750760,"confidence":0.99609375,"speaker":"A"},{"text":"schema","start":2750760,"end":2751200,"confidence":0.8933919,"speaker":"A"},{"text":"management","start":2751200,"end":2751480,"confidence":0.99121094,"speaker":"A"},{"text":"that","start":2752600,"end":2752880,"confidence":0.99902344,"speaker":"A"},{"text":"I","start":2752880,"end":2753000,"confidence":0.9658203,"speaker":"A"},{"text":"could.","start":2753000,"end":2753200,"confidence":0.8144531,"speaker":"A"}]},{"text":"That would be awesome if I could figure out how to do that. If I could figure out how to do key path query filtering, that would be fantastic.","start":2753200,"end":2759400,"confidence":0.99902344,"words":[{"text":"That","start":2753200,"end":2753440,"confidence":0.99902344,"speaker":"A"},{"text":"would","start":2753440,"end":2753560,"confidence":0.9995117,"speaker":"A"},{"text":"be","start":2753560,"end":2753680,"confidence":0.9995117,"speaker":"A"},{"text":"awesome","start":2753680,"end":2754080,"confidence":0.9998372,"speaker":"A"},{"text":"if","start":2754080,"end":2754320,"confidence":0.99902344,"speaker":"A"},{"text":"I","start":2754320,"end":2754440,"confidence":0.9995117,"speaker":"A"},{"text":"could","start":2754440,"end":2754640,"confidence":0.9863281,"speaker":"A"},{"text":"figure","start":2754640,"end":2754920,"confidence":1,"speaker":"A"},{"text":"out","start":2754920,"end":2755040,"confidence":0.9995117,"speaker":"A"},{"text":"how","start":2755040,"end":2755200,"confidence":0.9995117,"speaker":"A"},{"text":"to","start":2755200,"end":2755320,"confidence":1,"speaker":"A"},{"text":"do","start":2755320,"end":2755440,"confidence":0.9995117,"speaker":"A"},{"text":"that.","start":2755440,"end":2755720,"confidence":0.9995117,"speaker":"A"},{"text":"If","start":2755720,"end":2756000,"confidence":0.9995117,"speaker":"A"},{"text":"I","start":2756000,"end":2756120,"confidence":0.9995117,"speaker":"A"},{"text":"could","start":2756120,"end":2756240,"confidence":0.84375,"speaker":"A"},{"text":"figure","start":2756240,"end":2756440,"confidence":1,"speaker":"A"},{"text":"out","start":2756440,"end":2756520,"confidence":0.9995117,"speaker":"A"},{"text":"how","start":2756520,"end":2756600,"confidence":0.99853516,"speaker":"A"},{"text":"to","start":2756600,"end":2756680,"confidence":0.9975586,"speaker":"A"},{"text":"do","start":2756680,"end":2756800,"confidence":0.9921875,"speaker":"A"},{"text":"key","start":2756800,"end":2756960,"confidence":0.9682617,"speaker":"A"},{"text":"path","start":2756960,"end":2757280,"confidence":0.953125,"speaker":"A"},{"text":"query","start":2757280,"end":2757600,"confidence":0.9951172,"speaker":"A"},{"text":"filtering,","start":2757600,"end":2758120,"confidence":0.99934894,"speaker":"A"},{"text":"that","start":2758120,"end":2758320,"confidence":0.99902344,"speaker":"A"},{"text":"would","start":2758320,"end":2758480,"confidence":0.9995117,"speaker":"A"},{"text":"be","start":2758480,"end":2758640,"confidence":0.9995117,"speaker":"A"},{"text":"fantastic.","start":2758640,"end":2759400,"confidence":0.99890137,"speaker":"A"}]},{"text":"And yeah, but there's a. I mean the basics is there as far as if you want to do anything with a record, it's pretty much there. One thing with Celestra is I'd love to be able to do like test out subscriptions and see how that works. So yeah, that's really the bulk of my presentation today. Now is. Now it's time to ask me a ton of questions and make me feel dumb.","start":2761720,"end":2785480,"confidence":0.9951172,"words":[{"text":"And","start":2761720,"end":2762120,"confidence":0.9951172,"speaker":"A"},{"text":"yeah,","start":2762280,"end":2762760,"confidence":0.9998372,"speaker":"A"},{"text":"but","start":2762760,"end":2762960,"confidence":0.9995117,"speaker":"A"},{"text":"there's","start":2762960,"end":2763200,"confidence":0.87320966,"speaker":"A"},{"text":"a.","start":2763200,"end":2763400,"confidence":0.92626953,"speaker":"A"},{"text":"I","start":2763400,"end":2763560,"confidence":0.9980469,"speaker":"A"},{"text":"mean","start":2763560,"end":2763799,"confidence":0.79785156,"speaker":"A"},{"text":"the","start":2763799,"end":2764120,"confidence":0.9995117,"speaker":"A"},{"text":"basics","start":2764120,"end":2764520,"confidence":0.998291,"speaker":"A"},{"text":"is","start":2764520,"end":2764760,"confidence":0.9941406,"speaker":"A"},{"text":"there","start":2764760,"end":2765040,"confidence":0.9995117,"speaker":"A"},{"text":"as","start":2765040,"end":2765280,"confidence":0.9995117,"speaker":"A"},{"text":"far","start":2765280,"end":2765440,"confidence":1,"speaker":"A"},{"text":"as","start":2765440,"end":2765640,"confidence":0.9995117,"speaker":"A"},{"text":"if","start":2765640,"end":2765840,"confidence":0.9995117,"speaker":"A"},{"text":"you","start":2765840,"end":2765960,"confidence":0.99902344,"speaker":"A"},{"text":"want","start":2765960,"end":2766080,"confidence":0.77685547,"speaker":"A"},{"text":"to","start":2766080,"end":2766240,"confidence":0.9946289,"speaker":"A"},{"text":"do","start":2766240,"end":2766400,"confidence":1,"speaker":"A"},{"text":"anything","start":2766400,"end":2766760,"confidence":0.99975586,"speaker":"A"},{"text":"with","start":2766760,"end":2766960,"confidence":1,"speaker":"A"},{"text":"a","start":2766960,"end":2767120,"confidence":0.99560547,"speaker":"A"},{"text":"record,","start":2767120,"end":2767400,"confidence":0.99902344,"speaker":"A"},{"text":"it's","start":2768040,"end":2768400,"confidence":0.9983724,"speaker":"A"},{"text":"pretty","start":2768400,"end":2768600,"confidence":0.9998372,"speaker":"A"},{"text":"much","start":2768600,"end":2768760,"confidence":0.99853516,"speaker":"A"},{"text":"there.","start":2768760,"end":2769080,"confidence":0.98583984,"speaker":"A"},{"text":"One","start":2769720,"end":2770000,"confidence":0.9848633,"speaker":"A"},{"text":"thing","start":2770000,"end":2770160,"confidence":0.99853516,"speaker":"A"},{"text":"with","start":2770160,"end":2770320,"confidence":0.9995117,"speaker":"A"},{"text":"Celestra","start":2770320,"end":2770880,"confidence":0.7967122,"speaker":"A"},{"text":"is","start":2770880,"end":2771040,"confidence":0.8798828,"speaker":"A"},{"text":"I'd","start":2771040,"end":2771240,"confidence":0.9977214,"speaker":"A"},{"text":"love","start":2771240,"end":2771400,"confidence":0.9995117,"speaker":"A"},{"text":"to","start":2771400,"end":2771560,"confidence":0.9995117,"speaker":"A"},{"text":"be","start":2771560,"end":2771720,"confidence":0.99902344,"speaker":"A"},{"text":"able","start":2771720,"end":2771920,"confidence":0.9995117,"speaker":"A"},{"text":"to","start":2771920,"end":2772080,"confidence":1,"speaker":"A"},{"text":"do","start":2772080,"end":2772280,"confidence":1,"speaker":"A"},{"text":"like","start":2772280,"end":2772560,"confidence":0.99902344,"speaker":"A"},{"text":"test","start":2772560,"end":2772880,"confidence":0.99853516,"speaker":"A"},{"text":"out","start":2772880,"end":2773160,"confidence":0.9970703,"speaker":"A"},{"text":"subscriptions","start":2773160,"end":2773880,"confidence":0.9428711,"speaker":"A"},{"text":"and","start":2774200,"end":2774320,"confidence":0.94921875,"speaker":"A"},{"text":"see","start":2774320,"end":2774480,"confidence":0.9995117,"speaker":"A"},{"text":"how","start":2774480,"end":2774640,"confidence":1,"speaker":"A"},{"text":"that","start":2774640,"end":2774800,"confidence":1,"speaker":"A"},{"text":"works.","start":2774800,"end":2775240,"confidence":1,"speaker":"A"},{"text":"So","start":2775880,"end":2776280,"confidence":0.99609375,"speaker":"A"},{"text":"yeah,","start":2777320,"end":2777840,"confidence":0.9996745,"speaker":"A"},{"text":"that's","start":2777840,"end":2778200,"confidence":1,"speaker":"A"},{"text":"really","start":2778200,"end":2778360,"confidence":0.9995117,"speaker":"A"},{"text":"the","start":2778360,"end":2778560,"confidence":1,"speaker":"A"},{"text":"bulk","start":2778560,"end":2778800,"confidence":0.9817708,"speaker":"A"},{"text":"of","start":2778800,"end":2778960,"confidence":0.9995117,"speaker":"A"},{"text":"my","start":2778960,"end":2779120,"confidence":0.9995117,"speaker":"A"},{"text":"presentation","start":2779120,"end":2779720,"confidence":0.9995117,"speaker":"A"},{"text":"today.","start":2779720,"end":2780040,"confidence":0.99902344,"speaker":"A"},{"text":"Now","start":2781800,"end":2782160,"confidence":0.95751953,"speaker":"A"},{"text":"is.","start":2782160,"end":2782480,"confidence":0.8334961,"speaker":"A"},{"text":"Now","start":2782480,"end":2782720,"confidence":0.99902344,"speaker":"A"},{"text":"it's","start":2782720,"end":2782920,"confidence":0.99869794,"speaker":"A"},{"text":"time","start":2782920,"end":2783040,"confidence":0.9995117,"speaker":"A"},{"text":"to","start":2783040,"end":2783160,"confidence":0.9995117,"speaker":"A"},{"text":"ask","start":2783160,"end":2783280,"confidence":0.99902344,"speaker":"A"},{"text":"me","start":2783280,"end":2783440,"confidence":0.99658203,"speaker":"A"},{"text":"a","start":2783440,"end":2783560,"confidence":0.99902344,"speaker":"A"},{"text":"ton","start":2783560,"end":2783720,"confidence":0.9992676,"speaker":"A"},{"text":"of","start":2783720,"end":2783840,"confidence":0.9995117,"speaker":"A"},{"text":"questions","start":2783840,"end":2784200,"confidence":0.9995117,"speaker":"A"},{"text":"and","start":2784200,"end":2784480,"confidence":0.9814453,"speaker":"A"},{"text":"make","start":2784480,"end":2784720,"confidence":0.9995117,"speaker":"A"},{"text":"me","start":2784720,"end":2784880,"confidence":0.9995117,"speaker":"A"},{"text":"feel","start":2784880,"end":2785040,"confidence":1,"speaker":"A"},{"text":"dumb.","start":2785040,"end":2785480,"confidence":0.98706055,"speaker":"A"}]},{"text":"Go for it. No, there's a lot there to. To absorb. But I, I like the concept and I know you've been working on this for a while and I always thought it was a pretty cool, pretty cool idea and implementation of this. Questions?","start":2785880,"end":2803470,"confidence":0.99121094,"words":[{"text":"Go","start":2785880,"end":2786160,"confidence":0.99121094,"speaker":"A"},{"text":"for","start":2786160,"end":2786320,"confidence":0.9995117,"speaker":"A"},{"text":"it.","start":2786320,"end":2786600,"confidence":0.99853516,"speaker":"A"},{"text":"No,","start":2788440,"end":2788840,"confidence":0.95751953,"speaker":"B"},{"text":"there's","start":2789880,"end":2790319,"confidence":0.9355469,"speaker":"B"},{"text":"a","start":2790319,"end":2790440,"confidence":0.9995117,"speaker":"B"},{"text":"lot","start":2790440,"end":2790600,"confidence":0.9995117,"speaker":"B"},{"text":"there","start":2790600,"end":2790840,"confidence":0.99902344,"speaker":"B"},{"text":"to.","start":2790840,"end":2791160,"confidence":0.98828125,"speaker":"B"},{"text":"To","start":2791400,"end":2791720,"confidence":0.99902344,"speaker":"B"},{"text":"absorb.","start":2791720,"end":2792160,"confidence":0.99938965,"speaker":"B"},{"text":"But","start":2792160,"end":2792320,"confidence":0.9995117,"speaker":"B"},{"text":"I,","start":2792320,"end":2792600,"confidence":0.99121094,"speaker":"B"},{"text":"I","start":2792760,"end":2793120,"confidence":0.99658203,"speaker":"B"},{"text":"like","start":2793120,"end":2793400,"confidence":0.99902344,"speaker":"B"},{"text":"the","start":2793400,"end":2793640,"confidence":0.9995117,"speaker":"B"},{"text":"concept","start":2793640,"end":2794200,"confidence":0.976888,"speaker":"B"},{"text":"and","start":2794440,"end":2794720,"confidence":0.99560547,"speaker":"B"},{"text":"I","start":2794720,"end":2794840,"confidence":0.9995117,"speaker":"B"},{"text":"know","start":2794840,"end":2794960,"confidence":1,"speaker":"B"},{"text":"you've","start":2794960,"end":2795280,"confidence":0.99820966,"speaker":"B"},{"text":"been","start":2795280,"end":2795440,"confidence":0.9995117,"speaker":"B"},{"text":"working","start":2795440,"end":2795640,"confidence":0.9995117,"speaker":"B"},{"text":"on","start":2795640,"end":2795840,"confidence":0.9995117,"speaker":"B"},{"text":"this","start":2795840,"end":2796000,"confidence":0.9995117,"speaker":"B"},{"text":"for","start":2796000,"end":2796120,"confidence":0.9995117,"speaker":"B"},{"text":"a","start":2796120,"end":2796240,"confidence":0.99560547,"speaker":"B"},{"text":"while","start":2796240,"end":2796400,"confidence":1,"speaker":"B"},{"text":"and","start":2796400,"end":2796560,"confidence":0.9458008,"speaker":"B"},{"text":"I","start":2796560,"end":2796680,"confidence":0.9975586,"speaker":"B"},{"text":"always","start":2796680,"end":2796840,"confidence":0.99316406,"speaker":"B"},{"text":"thought","start":2796840,"end":2797040,"confidence":0.99853516,"speaker":"B"},{"text":"it","start":2797040,"end":2797160,"confidence":0.9970703,"speaker":"B"},{"text":"was","start":2797160,"end":2797280,"confidence":0.9951172,"speaker":"B"},{"text":"a","start":2797280,"end":2797440,"confidence":0.9663086,"speaker":"B"},{"text":"pretty","start":2797440,"end":2797640,"confidence":0.99869794,"speaker":"B"},{"text":"cool,","start":2797640,"end":2797960,"confidence":0.9980469,"speaker":"B"},{"text":"pretty","start":2799240,"end":2799560,"confidence":0.9943034,"speaker":"B"},{"text":"cool","start":2799560,"end":2799720,"confidence":0.88549805,"speaker":"B"},{"text":"idea","start":2800030,"end":2800350,"confidence":0.72094727,"speaker":"B"},{"text":"and","start":2800590,"end":2800910,"confidence":0.89404297,"speaker":"B"},{"text":"implementation","start":2800910,"end":2801630,"confidence":0.9941406,"speaker":"B"},{"text":"of","start":2801630,"end":2801910,"confidence":0.9770508,"speaker":"B"},{"text":"this.","start":2801910,"end":2802190,"confidence":0.9897461,"speaker":"B"},{"text":"Questions?","start":2802750,"end":2803470,"confidence":0.9904785,"speaker":"A"}]},{"text":"So with something like.","start":2808990,"end":2810030,"confidence":0.95214844,"words":[{"text":"So","start":2808990,"end":2809270,"confidence":0.95214844,"speaker":"C"},{"text":"with","start":2809270,"end":2809470,"confidence":0.9628906,"speaker":"C"},{"text":"something","start":2809470,"end":2809710,"confidence":0.9995117,"speaker":"C"},{"text":"like.","start":2809710,"end":2810030,"confidence":0.99853516,"speaker":"C"}]},{"text":"Accessing CloudKit through the web, is this setup more ideal for having your server do the authentication to CloudKit with Miskit or is miskit something that you could put into even like a client side, you know, like non Swift application or I guess not non Swift but like non like app application. I'm thinking in the context of like. A.","start":2814110,"end":2842049,"confidence":0.78027344,"words":[{"text":"Accessing","start":2814110,"end":2814750,"confidence":0.78027344,"speaker":"C"},{"text":"CloudKit","start":2814830,"end":2815430,"confidence":0.94202,"speaker":"C"},{"text":"through","start":2815430,"end":2815550,"confidence":0.9946289,"speaker":"C"},{"text":"the","start":2815550,"end":2815709,"confidence":0.99902344,"speaker":"C"},{"text":"web,","start":2815709,"end":2816109,"confidence":0.9916992,"speaker":"C"},{"text":"is","start":2816430,"end":2816830,"confidence":0.9995117,"speaker":"C"},{"text":"this","start":2817150,"end":2817510,"confidence":0.99853516,"speaker":"C"},{"text":"setup","start":2817510,"end":2817910,"confidence":0.95092773,"speaker":"C"},{"text":"more","start":2817910,"end":2818110,"confidence":0.9995117,"speaker":"C"},{"text":"ideal","start":2818110,"end":2818590,"confidence":0.9970703,"speaker":"C"},{"text":"for","start":2818670,"end":2819070,"confidence":0.9995117,"speaker":"C"},{"text":"having","start":2820270,"end":2820630,"confidence":0.9995117,"speaker":"C"},{"text":"your","start":2820630,"end":2820990,"confidence":1,"speaker":"C"},{"text":"server","start":2820990,"end":2821630,"confidence":1,"speaker":"C"},{"text":"do","start":2821870,"end":2822270,"confidence":0.9995117,"speaker":"C"},{"text":"the","start":2822670,"end":2822990,"confidence":0.9980469,"speaker":"C"},{"text":"authentication","start":2822990,"end":2823710,"confidence":1,"speaker":"C"},{"text":"to","start":2823950,"end":2824230,"confidence":0.9970703,"speaker":"C"},{"text":"CloudKit","start":2824230,"end":2824790,"confidence":0.9939,"speaker":"C"},{"text":"with","start":2824790,"end":2824950,"confidence":0.99560547,"speaker":"C"},{"text":"Miskit","start":2824950,"end":2825550,"confidence":0.9923096,"speaker":"C"},{"text":"or","start":2825970,"end":2826210,"confidence":0.9921875,"speaker":"C"},{"text":"is","start":2826290,"end":2826650,"confidence":0.9980469,"speaker":"C"},{"text":"miskit","start":2826650,"end":2827250,"confidence":0.93859863,"speaker":"C"},{"text":"something","start":2827250,"end":2827490,"confidence":0.99853516,"speaker":"C"},{"text":"that","start":2827490,"end":2827650,"confidence":0.99658203,"speaker":"C"},{"text":"you","start":2827650,"end":2827770,"confidence":0.9995117,"speaker":"C"},{"text":"could","start":2827770,"end":2827970,"confidence":0.9970703,"speaker":"C"},{"text":"put","start":2827970,"end":2828210,"confidence":0.9995117,"speaker":"C"},{"text":"into","start":2828210,"end":2828530,"confidence":0.99902344,"speaker":"C"},{"text":"even","start":2828530,"end":2828850,"confidence":0.99560547,"speaker":"C"},{"text":"like","start":2828850,"end":2829050,"confidence":0.9765625,"speaker":"C"},{"text":"a","start":2829050,"end":2829330,"confidence":0.5620117,"speaker":"C"},{"text":"client","start":2829330,"end":2829890,"confidence":0.9987793,"speaker":"C"},{"text":"side,","start":2830130,"end":2830530,"confidence":0.52978516,"speaker":"C"},{"text":"you","start":2832850,"end":2833170,"confidence":0.95751953,"speaker":"C"},{"text":"know,","start":2833170,"end":2833370,"confidence":0.9995117,"speaker":"C"},{"text":"like","start":2833370,"end":2833650,"confidence":0.98583984,"speaker":"C"},{"text":"non","start":2834690,"end":2835090,"confidence":0.99658203,"speaker":"C"},{"text":"Swift","start":2835810,"end":2836290,"confidence":0.99780273,"speaker":"C"},{"text":"application","start":2836290,"end":2836770,"confidence":0.9995117,"speaker":"C"},{"text":"or","start":2836770,"end":2837010,"confidence":0.9140625,"speaker":"C"},{"text":"I","start":2837010,"end":2837210,"confidence":0.6401367,"speaker":"C"},{"text":"guess","start":2837210,"end":2837490,"confidence":0.99975586,"speaker":"C"},{"text":"not","start":2837490,"end":2837730,"confidence":0.9628906,"speaker":"C"},{"text":"non","start":2837730,"end":2837930,"confidence":0.8105469,"speaker":"C"},{"text":"Swift","start":2837930,"end":2838250,"confidence":0.9489746,"speaker":"C"},{"text":"but","start":2838250,"end":2838410,"confidence":0.98876953,"speaker":"C"},{"text":"like","start":2838410,"end":2838610,"confidence":0.98583984,"speaker":"C"},{"text":"non","start":2838610,"end":2838930,"confidence":0.9560547,"speaker":"C"},{"text":"like","start":2839090,"end":2839410,"confidence":0.79785156,"speaker":"C"},{"text":"app","start":2839410,"end":2839690,"confidence":0.99609375,"speaker":"C"},{"text":"application.","start":2839690,"end":2840170,"confidence":0.99853516,"speaker":"C"},{"text":"I'm","start":2840170,"end":2840410,"confidence":0.99397784,"speaker":"C"},{"text":"thinking","start":2840410,"end":2840730,"confidence":0.8215332,"speaker":"C"},{"text":"in","start":2840730,"end":2840970,"confidence":0.6489258,"speaker":"C"},{"text":"the","start":2840970,"end":2841130,"confidence":0.9946289,"speaker":"C"},{"text":"context","start":2841130,"end":2841450,"confidence":0.98502606,"speaker":"C"},{"text":"of","start":2841450,"end":2841570,"confidence":0.99902344,"speaker":"C"},{"text":"like.","start":2841570,"end":2841730,"confidence":0.98876953,"speaker":"C"},{"text":"A.","start":2841730,"end":2842049,"confidence":0.71728516,"speaker":"A"}]},{"text":"I guess if I wanted to create a something accessing CloudKit that is not your typical Mac or iOS app. Can you be more specific? I'm looking into one. One approach would be browser extensions.","start":2845730,"end":2862560,"confidence":0.99658203,"words":[{"text":"I","start":2845730,"end":2845970,"confidence":0.99658203,"speaker":"C"},{"text":"guess","start":2845970,"end":2846170,"confidence":1,"speaker":"C"},{"text":"if","start":2846170,"end":2846290,"confidence":0.9970703,"speaker":"C"},{"text":"I","start":2846290,"end":2846410,"confidence":0.9995117,"speaker":"C"},{"text":"wanted","start":2846410,"end":2846730,"confidence":0.9848633,"speaker":"C"},{"text":"to","start":2846730,"end":2846930,"confidence":1,"speaker":"C"},{"text":"create","start":2846930,"end":2847250,"confidence":0.9995117,"speaker":"C"},{"text":"a","start":2847330,"end":2847730,"confidence":0.87939453,"speaker":"C"},{"text":"something","start":2849970,"end":2850290,"confidence":0.9970703,"speaker":"C"},{"text":"accessing","start":2850290,"end":2850810,"confidence":0.96655273,"speaker":"C"},{"text":"CloudKit","start":2850810,"end":2851330,"confidence":0.99853516,"speaker":"C"},{"text":"that","start":2851330,"end":2851490,"confidence":0.9995117,"speaker":"C"},{"text":"is","start":2851490,"end":2851610,"confidence":0.99902344,"speaker":"C"},{"text":"not","start":2851610,"end":2851810,"confidence":0.9995117,"speaker":"C"},{"text":"your","start":2851810,"end":2852010,"confidence":0.9995117,"speaker":"C"},{"text":"typical","start":2852010,"end":2852370,"confidence":1,"speaker":"C"},{"text":"Mac","start":2852370,"end":2852610,"confidence":0.99780273,"speaker":"C"},{"text":"or","start":2852610,"end":2852730,"confidence":0.9863281,"speaker":"C"},{"text":"iOS","start":2852730,"end":2853090,"confidence":0.9980469,"speaker":"C"},{"text":"app.","start":2853090,"end":2853410,"confidence":0.99853516,"speaker":"C"},{"text":"Can","start":2854880,"end":2855000,"confidence":0.9609375,"speaker":"A"},{"text":"you","start":2855000,"end":2855160,"confidence":0.8486328,"speaker":"A"},{"text":"be","start":2855160,"end":2855400,"confidence":0.9951172,"speaker":"A"},{"text":"more","start":2855400,"end":2855680,"confidence":1,"speaker":"A"},{"text":"specific?","start":2855680,"end":2856160,"confidence":0.99975586,"speaker":"A"},{"text":"I'm","start":2857840,"end":2858200,"confidence":0.99104816,"speaker":"C"},{"text":"looking","start":2858200,"end":2858480,"confidence":0.99902344,"speaker":"C"},{"text":"into","start":2858720,"end":2859120,"confidence":0.99560547,"speaker":"C"},{"text":"one.","start":2859280,"end":2859640,"confidence":0.45483398,"speaker":"C"},{"text":"One","start":2859640,"end":2859880,"confidence":1,"speaker":"C"},{"text":"approach","start":2859880,"end":2860120,"confidence":1,"speaker":"C"},{"text":"would","start":2860120,"end":2860400,"confidence":0.99560547,"speaker":"C"},{"text":"be","start":2860400,"end":2860720,"confidence":0.99853516,"speaker":"C"},{"text":"browser","start":2861600,"end":2862040,"confidence":0.9998372,"speaker":"C"},{"text":"extensions.","start":2862040,"end":2862560,"confidence":0.99869794,"speaker":"C"}]},{"text":"So for like a non Safari browser. Yes.","start":2865040,"end":2868240,"confidence":0.67871094,"words":[{"text":"So","start":2865040,"end":2865440,"confidence":0.67871094,"speaker":"A"},{"text":"for","start":2865680,"end":2866000,"confidence":0.9926758,"speaker":"A"},{"text":"like","start":2866000,"end":2866200,"confidence":0.9321289,"speaker":"A"},{"text":"a","start":2866200,"end":2866320,"confidence":0.99121094,"speaker":"A"},{"text":"non","start":2866320,"end":2866520,"confidence":0.99560547,"speaker":"A"},{"text":"Safari","start":2866520,"end":2867080,"confidence":0.9980469,"speaker":"A"},{"text":"browser.","start":2867080,"end":2867680,"confidence":0.99609375,"speaker":"A"},{"text":"Yes.","start":2867760,"end":2868240,"confidence":0.99121094,"speaker":"C"}]},{"text":"Yeah, this would be great. So basically the way you'd want that to work, like the sticky part to me would be getting the web authentication token. Other than that, like have at it.","start":2870400,"end":2881090,"confidence":0.9814453,"words":[{"text":"Yeah,","start":2870400,"end":2870720,"confidence":0.9814453,"speaker":"A"},{"text":"this","start":2870720,"end":2870840,"confidence":0.9975586,"speaker":"A"},{"text":"would","start":2870840,"end":2871000,"confidence":0.9975586,"speaker":"A"},{"text":"be","start":2871000,"end":2871160,"confidence":0.9995117,"speaker":"A"},{"text":"great.","start":2871160,"end":2871400,"confidence":1,"speaker":"A"},{"text":"So","start":2871400,"end":2871600,"confidence":0.96240234,"speaker":"A"},{"text":"basically","start":2871600,"end":2872000,"confidence":0.99902344,"speaker":"A"},{"text":"the","start":2873040,"end":2873320,"confidence":0.9995117,"speaker":"A"},{"text":"way","start":2873320,"end":2873560,"confidence":0.9995117,"speaker":"A"},{"text":"you'd","start":2873560,"end":2873960,"confidence":0.98860675,"speaker":"A"},{"text":"want","start":2873960,"end":2874120,"confidence":1,"speaker":"A"},{"text":"that","start":2874120,"end":2874320,"confidence":0.9995117,"speaker":"A"},{"text":"to","start":2874320,"end":2874560,"confidence":0.99853516,"speaker":"A"},{"text":"work,","start":2874560,"end":2874880,"confidence":0.99902344,"speaker":"A"},{"text":"like","start":2875040,"end":2875400,"confidence":0.73095703,"speaker":"A"},{"text":"the","start":2875400,"end":2875640,"confidence":0.9980469,"speaker":"A"},{"text":"sticky","start":2875640,"end":2876040,"confidence":0.9973958,"speaker":"A"},{"text":"part","start":2876040,"end":2876200,"confidence":0.9995117,"speaker":"A"},{"text":"to","start":2876200,"end":2876360,"confidence":0.9980469,"speaker":"A"},{"text":"me","start":2876360,"end":2876560,"confidence":0.9995117,"speaker":"A"},{"text":"would","start":2876560,"end":2876760,"confidence":0.9980469,"speaker":"A"},{"text":"be","start":2876760,"end":2876920,"confidence":0.9995117,"speaker":"A"},{"text":"getting","start":2876920,"end":2877120,"confidence":0.9980469,"speaker":"A"},{"text":"the","start":2877120,"end":2877320,"confidence":0.99902344,"speaker":"A"},{"text":"web","start":2877320,"end":2877560,"confidence":0.998291,"speaker":"A"},{"text":"authentication","start":2877560,"end":2878240,"confidence":0.92614746,"speaker":"A"},{"text":"token.","start":2878240,"end":2878640,"confidence":0.99934894,"speaker":"A"},{"text":"Other","start":2878640,"end":2878880,"confidence":0.99316406,"speaker":"A"},{"text":"than","start":2878880,"end":2879080,"confidence":0.99560547,"speaker":"A"},{"text":"that,","start":2879080,"end":2879360,"confidence":0.97509766,"speaker":"A"},{"text":"like","start":2879440,"end":2879840,"confidence":0.7050781,"speaker":"A"},{"text":"have","start":2880370,"end":2880530,"confidence":0.9765625,"speaker":"A"},{"text":"at","start":2880530,"end":2880770,"confidence":0.515625,"speaker":"A"},{"text":"it.","start":2880770,"end":2881090,"confidence":0.9980469,"speaker":"A"}]},{"text":"So I'm gonna, I'm gonna be devil's advocate. Why not just use the CloudKit JavaScript library. If it's an extension, my brain jumps to Swift first. Right. But it's the reason I'm asking that is like it's a, it's already a web extension.","start":2884610,"end":2900890,"confidence":0.97802734,"words":[{"text":"So","start":2884610,"end":2884890,"confidence":0.97802734,"speaker":"A"},{"text":"I'm","start":2884890,"end":2885050,"confidence":0.98339844,"speaker":"A"},{"text":"gonna,","start":2885050,"end":2885250,"confidence":0.8352051,"speaker":"A"},{"text":"I'm","start":2885250,"end":2885410,"confidence":0.9949544,"speaker":"A"},{"text":"gonna","start":2885410,"end":2885570,"confidence":0.9736328,"speaker":"A"},{"text":"be","start":2885570,"end":2885690,"confidence":0.99853516,"speaker":"A"},{"text":"devil's","start":2885690,"end":2886050,"confidence":0.9608154,"speaker":"A"},{"text":"advocate.","start":2886050,"end":2886610,"confidence":0.9995117,"speaker":"A"},{"text":"Why","start":2886690,"end":2887010,"confidence":0.99609375,"speaker":"A"},{"text":"not","start":2887010,"end":2887290,"confidence":1,"speaker":"A"},{"text":"just","start":2887290,"end":2887570,"confidence":0.9995117,"speaker":"A"},{"text":"use","start":2887570,"end":2887810,"confidence":0.9995117,"speaker":"A"},{"text":"the","start":2887810,"end":2888090,"confidence":0.9995117,"speaker":"A"},{"text":"CloudKit","start":2888090,"end":2888770,"confidence":0.87769,"speaker":"A"},{"text":"JavaScript","start":2888850,"end":2889730,"confidence":0.99454755,"speaker":"A"},{"text":"library.","start":2889730,"end":2890210,"confidence":0.8435872,"speaker":"A"},{"text":"If","start":2890210,"end":2890450,"confidence":0.5620117,"speaker":"C"},{"text":"it's","start":2890450,"end":2890690,"confidence":0.9998372,"speaker":"C"},{"text":"an","start":2890690,"end":2890890,"confidence":0.8232422,"speaker":"C"},{"text":"extension,","start":2890890,"end":2891490,"confidence":0.9998372,"speaker":"C"},{"text":"my","start":2892450,"end":2892770,"confidence":0.99853516,"speaker":"C"},{"text":"brain","start":2892770,"end":2893090,"confidence":1,"speaker":"C"},{"text":"jumps","start":2893090,"end":2893450,"confidence":0.9998372,"speaker":"C"},{"text":"to","start":2893450,"end":2893610,"confidence":0.9995117,"speaker":"C"},{"text":"Swift","start":2893610,"end":2893970,"confidence":0.9914551,"speaker":"C"},{"text":"first.","start":2893970,"end":2894290,"confidence":0.9975586,"speaker":"C"},{"text":"Right.","start":2895730,"end":2896129,"confidence":0.97021484,"speaker":"A"},{"text":"But","start":2896129,"end":2896410,"confidence":0.9995117,"speaker":"A"},{"text":"it's","start":2896410,"end":2896730,"confidence":0.96875,"speaker":"A"},{"text":"the","start":2896730,"end":2896970,"confidence":1,"speaker":"A"},{"text":"reason","start":2896970,"end":2897130,"confidence":0.99902344,"speaker":"A"},{"text":"I'm","start":2897130,"end":2897330,"confidence":0.9954427,"speaker":"A"},{"text":"asking","start":2897330,"end":2897610,"confidence":0.97094727,"speaker":"A"},{"text":"that","start":2897610,"end":2897810,"confidence":0.9765625,"speaker":"A"},{"text":"is","start":2897810,"end":2898090,"confidence":0.9980469,"speaker":"A"},{"text":"like","start":2898090,"end":2898370,"confidence":0.9921875,"speaker":"A"},{"text":"it's","start":2898370,"end":2898690,"confidence":0.9900716,"speaker":"A"},{"text":"a,","start":2898690,"end":2898930,"confidence":0.98291016,"speaker":"A"},{"text":"it's","start":2899410,"end":2899770,"confidence":0.9996745,"speaker":"A"},{"text":"already","start":2899770,"end":2899970,"confidence":0.9995117,"speaker":"A"},{"text":"a","start":2899970,"end":2900130,"confidence":0.9995117,"speaker":"A"},{"text":"web","start":2900130,"end":2900410,"confidence":0.98535156,"speaker":"A"},{"text":"extension.","start":2900410,"end":2900890,"confidence":0.9998372,"speaker":"A"}]},{"text":"I would assume that is true. That it's 90 web based or JavaScript based. So that's where I'm just like, well, you may as well. Like, I would love. I don't want to.","start":2900890,"end":2911320,"confidence":0.98535156,"words":[{"text":"I","start":2900890,"end":2901010,"confidence":0.98535156,"speaker":"A"},{"text":"would","start":2901010,"end":2901130,"confidence":0.98095703,"speaker":"A"},{"text":"assume","start":2901130,"end":2901410,"confidence":0.8614909,"speaker":"A"},{"text":"that","start":2901410,"end":2901570,"confidence":0.5854492,"speaker":"A"},{"text":"is","start":2901570,"end":2901690,"confidence":0.80126953,"speaker":"A"},{"text":"true.","start":2901690,"end":2902050,"confidence":0.9968262,"speaker":"A"},{"text":"That","start":2902690,"end":2903090,"confidence":0.9941406,"speaker":"A"},{"text":"it's","start":2903090,"end":2903490,"confidence":0.98876953,"speaker":"A"},{"text":"90","start":2903490,"end":2903810,"confidence":0.99951,"speaker":"A"},{"text":"web","start":2904290,"end":2904650,"confidence":0.9995117,"speaker":"A"},{"text":"based","start":2904650,"end":2904930,"confidence":0.99902344,"speaker":"A"},{"text":"or","start":2905090,"end":2905410,"confidence":0.99853516,"speaker":"A"},{"text":"JavaScript","start":2905410,"end":2906010,"confidence":0.998291,"speaker":"A"},{"text":"based.","start":2906010,"end":2906290,"confidence":0.99902344,"speaker":"A"},{"text":"So","start":2907120,"end":2907200,"confidence":0.9707031,"speaker":"A"},{"text":"that's","start":2907200,"end":2907360,"confidence":0.99934894,"speaker":"A"},{"text":"where","start":2907360,"end":2907480,"confidence":0.9506836,"speaker":"A"},{"text":"I'm","start":2907480,"end":2907680,"confidence":0.99886066,"speaker":"A"},{"text":"just","start":2907680,"end":2907800,"confidence":0.99560547,"speaker":"A"},{"text":"like,","start":2907800,"end":2908000,"confidence":0.99121094,"speaker":"A"},{"text":"well,","start":2908000,"end":2908320,"confidence":0.9951172,"speaker":"A"},{"text":"you","start":2908320,"end":2908600,"confidence":0.99902344,"speaker":"A"},{"text":"may","start":2908600,"end":2908760,"confidence":0.9995117,"speaker":"A"},{"text":"as","start":2908760,"end":2908920,"confidence":0.9995117,"speaker":"A"},{"text":"well.","start":2908920,"end":2909200,"confidence":0.9995117,"speaker":"A"},{"text":"Like,","start":2909200,"end":2909600,"confidence":0.5307617,"speaker":"A"},{"text":"I","start":2909840,"end":2910120,"confidence":0.77685547,"speaker":"A"},{"text":"would","start":2910120,"end":2910280,"confidence":0.99609375,"speaker":"A"},{"text":"love.","start":2910280,"end":2910560,"confidence":0.99853516,"speaker":"A"},{"text":"I","start":2910640,"end":2910880,"confidence":0.97021484,"speaker":"A"},{"text":"don't","start":2910880,"end":2911000,"confidence":0.9313151,"speaker":"A"},{"text":"want","start":2911000,"end":2911120,"confidence":0.9394531,"speaker":"A"},{"text":"to.","start":2911120,"end":2911320,"confidence":0.94433594,"speaker":"A"}]},{"text":"Like, I love tooting my own horn. Right. But like, like why not just. Unless you're.","start":2911320,"end":2917120,"confidence":0.81689453,"words":[{"text":"Like,","start":2911320,"end":2911560,"confidence":0.81689453,"speaker":"A"},{"text":"I","start":2911560,"end":2911680,"confidence":0.99658203,"speaker":"A"},{"text":"love","start":2911680,"end":2911800,"confidence":0.99365234,"speaker":"A"},{"text":"tooting","start":2911800,"end":2912160,"confidence":0.8005371,"speaker":"A"},{"text":"my","start":2912160,"end":2912320,"confidence":1,"speaker":"A"},{"text":"own","start":2912320,"end":2912480,"confidence":1,"speaker":"A"},{"text":"horn.","start":2912480,"end":2912800,"confidence":0.9995117,"speaker":"A"},{"text":"Right.","start":2912800,"end":2913040,"confidence":0.9838867,"speaker":"A"},{"text":"But","start":2913040,"end":2913280,"confidence":0.9951172,"speaker":"A"},{"text":"like,","start":2913280,"end":2913600,"confidence":0.94628906,"speaker":"A"},{"text":"like","start":2914880,"end":2915280,"confidence":0.82666016,"speaker":"A"},{"text":"why","start":2915280,"end":2915560,"confidence":0.9951172,"speaker":"A"},{"text":"not","start":2915560,"end":2915800,"confidence":0.87939453,"speaker":"A"},{"text":"just.","start":2915800,"end":2916160,"confidence":0.9975586,"speaker":"A"},{"text":"Unless","start":2916320,"end":2916720,"confidence":0.92749023,"speaker":"A"},{"text":"you're.","start":2916720,"end":2917120,"confidence":0.9876302,"speaker":"A"}]},{"text":"Unless you're like building a executable, I guess, or an app. Ish. And I guess another application for this would be doing CloudKit stuff server side and then providing my own API layer over it. Yep, yep. So that's.","start":2920720,"end":2939860,"confidence":0.998291,"words":[{"text":"Unless","start":2920720,"end":2921080,"confidence":0.998291,"speaker":"A"},{"text":"you're","start":2921080,"end":2921440,"confidence":0.90478516,"speaker":"A"},{"text":"like","start":2921440,"end":2921840,"confidence":0.94628906,"speaker":"A"},{"text":"building","start":2922000,"end":2922400,"confidence":1,"speaker":"A"},{"text":"a","start":2922480,"end":2922879,"confidence":0.6621094,"speaker":"A"},{"text":"executable,","start":2923040,"end":2923840,"confidence":0.9987793,"speaker":"A"},{"text":"I","start":2924160,"end":2924440,"confidence":0.99316406,"speaker":"A"},{"text":"guess,","start":2924440,"end":2924800,"confidence":1,"speaker":"A"},{"text":"or","start":2924800,"end":2925080,"confidence":0.9970703,"speaker":"A"},{"text":"an","start":2925080,"end":2925240,"confidence":0.9628906,"speaker":"A"},{"text":"app.","start":2925240,"end":2925480,"confidence":0.93652344,"speaker":"A"},{"text":"Ish.","start":2925480,"end":2925920,"confidence":0.7595215,"speaker":"A"},{"text":"And","start":2927760,"end":2928080,"confidence":0.9038086,"speaker":"C"},{"text":"I","start":2928080,"end":2928400,"confidence":0.64697266,"speaker":"C"},{"text":"guess","start":2928400,"end":2928800,"confidence":1,"speaker":"C"},{"text":"another","start":2928800,"end":2929120,"confidence":1,"speaker":"C"},{"text":"application","start":2929120,"end":2929760,"confidence":1,"speaker":"C"},{"text":"for","start":2929760,"end":2930000,"confidence":1,"speaker":"C"},{"text":"this","start":2930000,"end":2930240,"confidence":1,"speaker":"C"},{"text":"would","start":2930240,"end":2930560,"confidence":0.9995117,"speaker":"C"},{"text":"be","start":2930560,"end":2930960,"confidence":0.9995117,"speaker":"C"},{"text":"doing","start":2931680,"end":2932040,"confidence":0.9995117,"speaker":"C"},{"text":"CloudKit","start":2932040,"end":2932680,"confidence":0.99902344,"speaker":"C"},{"text":"stuff","start":2932680,"end":2933000,"confidence":0.9954427,"speaker":"C"},{"text":"server","start":2933000,"end":2933360,"confidence":0.9074707,"speaker":"C"},{"text":"side","start":2933360,"end":2933640,"confidence":1,"speaker":"C"},{"text":"and","start":2933640,"end":2934000,"confidence":0.9243164,"speaker":"C"},{"text":"then","start":2934000,"end":2934400,"confidence":0.9995117,"speaker":"C"},{"text":"providing","start":2934400,"end":2934880,"confidence":0.8515625,"speaker":"C"},{"text":"my","start":2934880,"end":2935120,"confidence":0.9995117,"speaker":"C"},{"text":"own","start":2935120,"end":2935400,"confidence":1,"speaker":"C"},{"text":"API","start":2935400,"end":2935920,"confidence":1,"speaker":"C"},{"text":"layer","start":2935920,"end":2936280,"confidence":0.9995117,"speaker":"C"},{"text":"over","start":2936280,"end":2936480,"confidence":1,"speaker":"C"},{"text":"it.","start":2936480,"end":2936800,"confidence":0.99853516,"speaker":"C"},{"text":"Yep,","start":2937660,"end":2938060,"confidence":0.8959961,"speaker":"A"},{"text":"yep.","start":2938220,"end":2938700,"confidence":0.7453613,"speaker":"A"},{"text":"So","start":2938940,"end":2939340,"confidence":0.9946289,"speaker":"A"},{"text":"that's.","start":2939340,"end":2939860,"confidence":0.9943034,"speaker":"A"}]},{"text":"Yeah. Are we talking private database or public database? Private. So in that case, basically like you'd have to go the Hard Twitch route and you would have to provide a way to get their web authentication token, essentially, if that makes sense. And then store it in Postgres or whatever the hell you want to do.","start":2939860,"end":2963260,"confidence":0.99316406,"words":[{"text":"Yeah.","start":2939860,"end":2940300,"confidence":0.99316406,"speaker":"A"},{"text":"Are","start":2940460,"end":2940700,"confidence":0.99658203,"speaker":"A"},{"text":"we","start":2940700,"end":2940820,"confidence":0.9995117,"speaker":"A"},{"text":"talking","start":2940820,"end":2941180,"confidence":0.9992676,"speaker":"A"},{"text":"private","start":2941340,"end":2941660,"confidence":0.99902344,"speaker":"A"},{"text":"database","start":2941660,"end":2942180,"confidence":0.9998372,"speaker":"A"},{"text":"or","start":2942180,"end":2942340,"confidence":0.9970703,"speaker":"A"},{"text":"public","start":2942340,"end":2942540,"confidence":0.9995117,"speaker":"A"},{"text":"database?","start":2942540,"end":2943180,"confidence":0.9995117,"speaker":"A"},{"text":"Private.","start":2943340,"end":2943740,"confidence":0.99609375,"speaker":"C"},{"text":"So","start":2945580,"end":2945820,"confidence":0.99902344,"speaker":"A"},{"text":"in","start":2945820,"end":2945940,"confidence":0.9995117,"speaker":"A"},{"text":"that","start":2945940,"end":2946140,"confidence":0.9995117,"speaker":"A"},{"text":"case,","start":2946140,"end":2946460,"confidence":1,"speaker":"A"},{"text":"basically","start":2946700,"end":2947340,"confidence":0.99975586,"speaker":"A"},{"text":"like","start":2948060,"end":2948340,"confidence":0.99853516,"speaker":"A"},{"text":"you'd","start":2948340,"end":2948660,"confidence":0.99690753,"speaker":"A"},{"text":"have","start":2948660,"end":2948780,"confidence":1,"speaker":"A"},{"text":"to","start":2948780,"end":2948900,"confidence":1,"speaker":"A"},{"text":"go","start":2948900,"end":2949140,"confidence":0.9995117,"speaker":"A"},{"text":"the","start":2949140,"end":2949380,"confidence":0.99902344,"speaker":"A"},{"text":"Hard","start":2949380,"end":2949580,"confidence":0.8798828,"speaker":"A"},{"text":"Twitch","start":2949580,"end":2949940,"confidence":0.9433594,"speaker":"A"},{"text":"route","start":2949940,"end":2950300,"confidence":0.9946289,"speaker":"A"},{"text":"and","start":2951100,"end":2951500,"confidence":0.9951172,"speaker":"A"},{"text":"you","start":2952460,"end":2952740,"confidence":0.99853516,"speaker":"A"},{"text":"would","start":2952740,"end":2952979,"confidence":0.8515625,"speaker":"A"},{"text":"have","start":2952979,"end":2953219,"confidence":1,"speaker":"A"},{"text":"to","start":2953219,"end":2953380,"confidence":1,"speaker":"A"},{"text":"provide","start":2953380,"end":2953660,"confidence":1,"speaker":"A"},{"text":"a","start":2953900,"end":2954180,"confidence":0.9760742,"speaker":"A"},{"text":"way","start":2954180,"end":2954460,"confidence":0.9975586,"speaker":"A"},{"text":"to","start":2955980,"end":2956260,"confidence":0.9975586,"speaker":"A"},{"text":"get","start":2956260,"end":2956420,"confidence":1,"speaker":"A"},{"text":"their","start":2956420,"end":2956580,"confidence":0.9921875,"speaker":"A"},{"text":"web","start":2956580,"end":2956820,"confidence":0.9992676,"speaker":"A"},{"text":"authentication","start":2956820,"end":2957420,"confidence":0.9996338,"speaker":"A"},{"text":"token,","start":2957420,"end":2957980,"confidence":0.99820966,"speaker":"A"},{"text":"essentially,","start":2958460,"end":2959060,"confidence":0.9316406,"speaker":"A"},{"text":"if","start":2959060,"end":2959260,"confidence":0.9770508,"speaker":"A"},{"text":"that","start":2959260,"end":2959380,"confidence":0.9995117,"speaker":"A"},{"text":"makes","start":2959380,"end":2959540,"confidence":0.9970703,"speaker":"A"},{"text":"sense.","start":2959540,"end":2959900,"confidence":0.99853516,"speaker":"A"},{"text":"And","start":2960540,"end":2960820,"confidence":0.9975586,"speaker":"A"},{"text":"then","start":2960820,"end":2961020,"confidence":0.99902344,"speaker":"A"},{"text":"store","start":2961020,"end":2961260,"confidence":0.99853516,"speaker":"A"},{"text":"it","start":2961260,"end":2961380,"confidence":0.9980469,"speaker":"A"},{"text":"in","start":2961380,"end":2961540,"confidence":0.9980469,"speaker":"A"},{"text":"Postgres","start":2961540,"end":2962020,"confidence":0.98046875,"speaker":"A"},{"text":"or","start":2962020,"end":2962180,"confidence":0.9970703,"speaker":"A"},{"text":"whatever","start":2962180,"end":2962380,"confidence":0.9995117,"speaker":"A"},{"text":"the","start":2962380,"end":2962500,"confidence":0.99902344,"speaker":"A"},{"text":"hell","start":2962500,"end":2962700,"confidence":0.99975586,"speaker":"A"},{"text":"you","start":2962700,"end":2962820,"confidence":0.9995117,"speaker":"A"},{"text":"want","start":2962820,"end":2962980,"confidence":0.97802734,"speaker":"A"},{"text":"to","start":2962980,"end":2963100,"confidence":0.9980469,"speaker":"A"},{"text":"do.","start":2963100,"end":2963260,"confidence":0.9995117,"speaker":"A"}]},{"text":"Like that's, that's the way I did it with Hard Twitch. But once you have that, you can do anything you want on the server with their private database, if that makes sense. It does. Yep. Yep.","start":2963260,"end":2975120,"confidence":0.99121094,"words":[{"text":"Like","start":2963260,"end":2963500,"confidence":0.99121094,"speaker":"A"},{"text":"that's,","start":2963500,"end":2963820,"confidence":0.98876953,"speaker":"A"},{"text":"that's","start":2963820,"end":2964060,"confidence":0.99658203,"speaker":"A"},{"text":"the","start":2964060,"end":2964140,"confidence":0.99902344,"speaker":"A"},{"text":"way","start":2964140,"end":2964220,"confidence":1,"speaker":"A"},{"text":"I","start":2964220,"end":2964340,"confidence":0.9995117,"speaker":"A"},{"text":"did","start":2964340,"end":2964460,"confidence":0.9941406,"speaker":"A"},{"text":"it","start":2964460,"end":2964540,"confidence":0.9946289,"speaker":"A"},{"text":"with","start":2964540,"end":2964660,"confidence":0.9995117,"speaker":"A"},{"text":"Hard","start":2964660,"end":2964820,"confidence":0.8378906,"speaker":"A"},{"text":"Twitch.","start":2964820,"end":2965260,"confidence":0.88256836,"speaker":"A"},{"text":"But","start":2966400,"end":2966480,"confidence":0.96484375,"speaker":"A"},{"text":"once","start":2966480,"end":2966600,"confidence":0.9897461,"speaker":"A"},{"text":"you","start":2966600,"end":2966760,"confidence":0.9946289,"speaker":"A"},{"text":"have","start":2966760,"end":2966880,"confidence":0.8364258,"speaker":"A"},{"text":"that,","start":2966880,"end":2967120,"confidence":0.5385742,"speaker":"A"},{"text":"you","start":2967120,"end":2967360,"confidence":0.9995117,"speaker":"A"},{"text":"can","start":2967360,"end":2967440,"confidence":0.99902344,"speaker":"A"},{"text":"do","start":2967440,"end":2967520,"confidence":0.9995117,"speaker":"A"},{"text":"anything","start":2967520,"end":2967760,"confidence":0.9995117,"speaker":"A"},{"text":"you","start":2967760,"end":2967880,"confidence":0.9970703,"speaker":"A"},{"text":"want","start":2967880,"end":2968080,"confidence":0.99658203,"speaker":"A"},{"text":"on","start":2968080,"end":2968280,"confidence":0.99853516,"speaker":"A"},{"text":"the","start":2968280,"end":2968440,"confidence":0.99316406,"speaker":"A"},{"text":"server","start":2968440,"end":2968880,"confidence":0.99975586,"speaker":"A"},{"text":"with","start":2969200,"end":2969520,"confidence":0.9980469,"speaker":"A"},{"text":"their","start":2969520,"end":2969840,"confidence":0.98583984,"speaker":"A"},{"text":"private","start":2970240,"end":2970600,"confidence":0.99853516,"speaker":"A"},{"text":"database,","start":2970600,"end":2971200,"confidence":0.9996745,"speaker":"A"},{"text":"if","start":2971200,"end":2971400,"confidence":0.99902344,"speaker":"A"},{"text":"that","start":2971400,"end":2971560,"confidence":0.9995117,"speaker":"A"},{"text":"makes","start":2971560,"end":2971720,"confidence":0.9970703,"speaker":"A"},{"text":"sense.","start":2971720,"end":2972080,"confidence":0.99902344,"speaker":"A"},{"text":"It","start":2972560,"end":2972840,"confidence":0.9692383,"speaker":"C"},{"text":"does.","start":2972840,"end":2973120,"confidence":0.9980469,"speaker":"C"},{"text":"Yep.","start":2973920,"end":2974480,"confidence":0.8156738,"speaker":"A"},{"text":"Yep.","start":2974560,"end":2975120,"confidence":0.7368164,"speaker":"A"}]},{"text":"A couple of things I wanted to bring up, so let's take a look.","start":2975920,"end":2979520,"confidence":0.5620117,"words":[{"text":"A","start":2975920,"end":2976160,"confidence":0.5620117,"speaker":"A"},{"text":"couple","start":2976160,"end":2976360,"confidence":0.99731445,"speaker":"A"},{"text":"of","start":2976360,"end":2976480,"confidence":0.9433594,"speaker":"A"},{"text":"things","start":2976480,"end":2976720,"confidence":0.99902344,"speaker":"A"},{"text":"I","start":2977040,"end":2977320,"confidence":0.9980469,"speaker":"A"},{"text":"wanted","start":2977320,"end":2977560,"confidence":0.9992676,"speaker":"A"},{"text":"to","start":2977560,"end":2977720,"confidence":0.9995117,"speaker":"A"},{"text":"bring","start":2977720,"end":2977920,"confidence":1,"speaker":"A"},{"text":"up,","start":2977920,"end":2978240,"confidence":0.9995117,"speaker":"A"},{"text":"so","start":2978320,"end":2978640,"confidence":0.9765625,"speaker":"A"},{"text":"let's","start":2978640,"end":2978920,"confidence":0.99902344,"speaker":"A"},{"text":"take","start":2978920,"end":2979080,"confidence":1,"speaker":"A"},{"text":"a","start":2979080,"end":2979240,"confidence":1,"speaker":"A"},{"text":"look.","start":2979240,"end":2979520,"confidence":0.9995117,"speaker":"A"}]},{"text":"So part of my other presentation is working, talking about cross platform automation type stuff. And the one issue I've run into is. So it basically builds on everything. Right now.","start":2984000,"end":3001560,"confidence":0.95214844,"words":[{"text":"So","start":2984000,"end":2984400,"confidence":0.95214844,"speaker":"A"},{"text":"part","start":2986880,"end":2987160,"confidence":0.99902344,"speaker":"A"},{"text":"of","start":2987160,"end":2987280,"confidence":1,"speaker":"A"},{"text":"my","start":2987280,"end":2987400,"confidence":1,"speaker":"A"},{"text":"other","start":2987400,"end":2987640,"confidence":1,"speaker":"A"},{"text":"presentation","start":2987640,"end":2988400,"confidence":1,"speaker":"A"},{"text":"is","start":2988640,"end":2989040,"confidence":0.99853516,"speaker":"A"},{"text":"working,","start":2990000,"end":2990400,"confidence":0.87841797,"speaker":"A"},{"text":"talking","start":2990800,"end":2991160,"confidence":0.7766113,"speaker":"A"},{"text":"about","start":2991160,"end":2991440,"confidence":0.9951172,"speaker":"A"},{"text":"cross","start":2991640,"end":2991880,"confidence":0.998291,"speaker":"A"},{"text":"platform","start":2991880,"end":2992360,"confidence":0.8640137,"speaker":"A"},{"text":"automation","start":2992600,"end":2993320,"confidence":0.9996745,"speaker":"A"},{"text":"type","start":2993640,"end":2994000,"confidence":0.9980469,"speaker":"A"},{"text":"stuff.","start":2994000,"end":2994440,"confidence":1,"speaker":"A"},{"text":"And","start":2995560,"end":2995960,"confidence":0.9868164,"speaker":"A"},{"text":"the","start":2996440,"end":2996760,"confidence":0.9995117,"speaker":"A"},{"text":"one","start":2996760,"end":2997040,"confidence":1,"speaker":"A"},{"text":"issue","start":2997040,"end":2997400,"confidence":0.9995117,"speaker":"A"},{"text":"I've","start":2997400,"end":2997840,"confidence":0.9972331,"speaker":"A"},{"text":"run","start":2997840,"end":2998040,"confidence":0.9995117,"speaker":"A"},{"text":"into","start":2998040,"end":2998360,"confidence":1,"speaker":"A"},{"text":"is.","start":2998440,"end":2998840,"confidence":0.9926758,"speaker":"A"},{"text":"So","start":2998920,"end":2999200,"confidence":0.9921875,"speaker":"A"},{"text":"it","start":2999200,"end":2999360,"confidence":0.9916992,"speaker":"A"},{"text":"basically","start":2999360,"end":2999800,"confidence":0.99975586,"speaker":"A"},{"text":"builds","start":2999800,"end":3000160,"confidence":0.9992676,"speaker":"A"},{"text":"on","start":3000160,"end":3000360,"confidence":0.9995117,"speaker":"A"},{"text":"everything.","start":3000360,"end":3000680,"confidence":1,"speaker":"A"},{"text":"Right","start":3000920,"end":3001240,"confidence":0.9995117,"speaker":"A"},{"text":"now.","start":3001240,"end":3001560,"confidence":0.9995117,"speaker":"A"}]},{"text":"I'm going to share something. Hey guys, I got to drop. But it was good presentation, Leo. Thank you. Yeah, yeah.","start":3007560,"end":3015560,"confidence":0.9977214,"words":[{"text":"I'm","start":3007560,"end":3007880,"confidence":0.9977214,"speaker":"A"},{"text":"going","start":3007880,"end":3007960,"confidence":0.6772461,"speaker":"A"},{"text":"to","start":3007960,"end":3008080,"confidence":0.9975586,"speaker":"A"},{"text":"share","start":3008080,"end":3008320,"confidence":0.9995117,"speaker":"A"},{"text":"something.","start":3008320,"end":3008680,"confidence":0.9995117,"speaker":"A"},{"text":"Hey","start":3009880,"end":3010200,"confidence":0.99609375,"speaker":"B"},{"text":"guys,","start":3010200,"end":3010520,"confidence":0.99902344,"speaker":"B"},{"text":"I","start":3011000,"end":3011240,"confidence":0.9770508,"speaker":"B"},{"text":"got","start":3011240,"end":3011320,"confidence":0.99609375,"speaker":"B"},{"text":"to","start":3011320,"end":3011400,"confidence":0.44458008,"speaker":"B"},{"text":"drop.","start":3011400,"end":3011720,"confidence":0.9885254,"speaker":"B"},{"text":"But","start":3011800,"end":3012160,"confidence":0.98291016,"speaker":"B"},{"text":"it","start":3012160,"end":3012400,"confidence":0.9995117,"speaker":"B"},{"text":"was","start":3012400,"end":3012680,"confidence":0.9995117,"speaker":"B"},{"text":"good","start":3012680,"end":3013000,"confidence":0.9995117,"speaker":"B"},{"text":"presentation,","start":3013000,"end":3013480,"confidence":0.9995117,"speaker":"B"},{"text":"Leo.","start":3013480,"end":3014040,"confidence":0.9987793,"speaker":"B"},{"text":"Thank","start":3014040,"end":3014400,"confidence":0.99975586,"speaker":"B"},{"text":"you.","start":3014400,"end":3014680,"confidence":0.9975586,"speaker":"B"},{"text":"Yeah,","start":3014840,"end":3015240,"confidence":0.99088544,"speaker":"A"},{"text":"yeah.","start":3015240,"end":3015560,"confidence":0.9458008,"speaker":"A"}]},{"text":"If I have more questions, if you have any feedback, just hit me up on Slack. Sounds good. Cool, thank you. Thank you so much for helping me set this up. Yeah, talk to you later.","start":3015560,"end":3024710,"confidence":0.88964844,"words":[{"text":"If","start":3015560,"end":3015720,"confidence":0.88964844,"speaker":"A"},{"text":"I","start":3015720,"end":3015840,"confidence":0.98876953,"speaker":"A"},{"text":"have","start":3015840,"end":3015960,"confidence":0.9169922,"speaker":"A"},{"text":"more","start":3015960,"end":3016040,"confidence":0.97265625,"speaker":"A"},{"text":"questions,","start":3016040,"end":3016320,"confidence":0.95996094,"speaker":"A"},{"text":"if","start":3016320,"end":3016440,"confidence":0.9589844,"speaker":"A"},{"text":"you","start":3016440,"end":3016520,"confidence":0.9951172,"speaker":"A"},{"text":"have","start":3016520,"end":3016640,"confidence":0.9980469,"speaker":"A"},{"text":"any","start":3016640,"end":3016800,"confidence":0.9995117,"speaker":"A"},{"text":"feedback,","start":3016800,"end":3017160,"confidence":0.9996338,"speaker":"A"},{"text":"just","start":3017160,"end":3017360,"confidence":0.9995117,"speaker":"A"},{"text":"hit","start":3017360,"end":3017520,"confidence":1,"speaker":"A"},{"text":"me","start":3017520,"end":3017640,"confidence":1,"speaker":"A"},{"text":"up","start":3017640,"end":3017760,"confidence":1,"speaker":"A"},{"text":"on","start":3017760,"end":3018040,"confidence":0.99658203,"speaker":"A"},{"text":"Slack.","start":3018950,"end":3019350,"confidence":0.89697266,"speaker":"A"},{"text":"Sounds","start":3019590,"end":3019990,"confidence":0.9978841,"speaker":"B"},{"text":"good.","start":3019990,"end":3020150,"confidence":0.9980469,"speaker":"B"},{"text":"Cool,","start":3020150,"end":3020470,"confidence":0.9345703,"speaker":"A"},{"text":"thank","start":3020470,"end":3020750,"confidence":0.7890625,"speaker":"A"},{"text":"you.","start":3020750,"end":3020950,"confidence":0.99316406,"speaker":"A"},{"text":"Thank","start":3020950,"end":3021230,"confidence":0.94628906,"speaker":"A"},{"text":"you","start":3021230,"end":3021350,"confidence":0.9995117,"speaker":"A"},{"text":"so","start":3021350,"end":3021470,"confidence":0.99853516,"speaker":"A"},{"text":"much","start":3021470,"end":3021590,"confidence":1,"speaker":"A"},{"text":"for","start":3021590,"end":3021710,"confidence":0.9995117,"speaker":"A"},{"text":"helping","start":3021710,"end":3021950,"confidence":0.99975586,"speaker":"A"},{"text":"me","start":3021950,"end":3022150,"confidence":0.81103516,"speaker":"A"},{"text":"set","start":3022150,"end":3022350,"confidence":0.96240234,"speaker":"A"},{"text":"this","start":3022350,"end":3022510,"confidence":0.99365234,"speaker":"A"},{"text":"up.","start":3022510,"end":3022790,"confidence":0.99902344,"speaker":"A"},{"text":"Yeah,","start":3023590,"end":3023990,"confidence":0.95214844,"speaker":"A"},{"text":"talk","start":3023990,"end":3024190,"confidence":0.9824219,"speaker":"A"},{"text":"to","start":3024190,"end":3024350,"confidence":0.99902344,"speaker":"A"},{"text":"you","start":3024350,"end":3024470,"confidence":0.99658203,"speaker":"A"},{"text":"later.","start":3024470,"end":3024710,"confidence":0.9838867,"speaker":"A"}]},{"text":"Thank you. Bye bye.","start":3024950,"end":3025910,"confidence":0.9968262,"words":[{"text":"Thank","start":3024950,"end":3025230,"confidence":0.9968262,"speaker":"B"},{"text":"you.","start":3025230,"end":3025350,"confidence":0.99902344,"speaker":"B"},{"text":"Bye","start":3025350,"end":3025590,"confidence":0.9824219,"speaker":"B"},{"text":"bye.","start":3025590,"end":3025910,"confidence":0.99316406,"speaker":"B"}]},{"text":"Yeah, so if you had something else to show, I'm happy to look for. I'm here for a few more minutes as well. Yeah, yeah, yeah.","start":3028870,"end":3034390,"confidence":0.88216144,"words":[{"text":"Yeah,","start":3028870,"end":3029190,"confidence":0.88216144,"speaker":"C"},{"text":"so","start":3029190,"end":3029310,"confidence":0.91308594,"speaker":"C"},{"text":"if","start":3029310,"end":3029430,"confidence":0.99609375,"speaker":"C"},{"text":"you","start":3029430,"end":3029510,"confidence":0.99365234,"speaker":"C"},{"text":"had","start":3029510,"end":3029630,"confidence":0.9638672,"speaker":"C"},{"text":"something","start":3029630,"end":3029830,"confidence":0.9995117,"speaker":"C"},{"text":"else","start":3029830,"end":3030070,"confidence":0.99975586,"speaker":"C"},{"text":"to","start":3030070,"end":3030190,"confidence":0.99853516,"speaker":"C"},{"text":"show,","start":3030190,"end":3030350,"confidence":0.99902344,"speaker":"C"},{"text":"I'm","start":3030350,"end":3030550,"confidence":0.99869794,"speaker":"C"},{"text":"happy","start":3030550,"end":3030750,"confidence":0.9995117,"speaker":"C"},{"text":"to","start":3030750,"end":3030990,"confidence":0.6503906,"speaker":"C"},{"text":"look","start":3030990,"end":3031230,"confidence":0.97021484,"speaker":"C"},{"text":"for.","start":3031230,"end":3031430,"confidence":0.79541016,"speaker":"C"},{"text":"I'm","start":3031430,"end":3031670,"confidence":0.99104816,"speaker":"C"},{"text":"here","start":3031670,"end":3031790,"confidence":0.9995117,"speaker":"C"},{"text":"for","start":3031790,"end":3031910,"confidence":0.9995117,"speaker":"C"},{"text":"a","start":3031910,"end":3031990,"confidence":0.9980469,"speaker":"C"},{"text":"few","start":3031990,"end":3032110,"confidence":0.9995117,"speaker":"C"},{"text":"more","start":3032110,"end":3032270,"confidence":0.9995117,"speaker":"C"},{"text":"minutes","start":3032270,"end":3032510,"confidence":0.9987793,"speaker":"C"},{"text":"as","start":3032510,"end":3032670,"confidence":0.99853516,"speaker":"C"},{"text":"well.","start":3032670,"end":3032950,"confidence":0.99902344,"speaker":"C"},{"text":"Yeah,","start":3033590,"end":3033910,"confidence":0.96402997,"speaker":"A"},{"text":"yeah,","start":3033910,"end":3034070,"confidence":0.90755206,"speaker":"A"},{"text":"yeah.","start":3034070,"end":3034390,"confidence":0.8152669,"speaker":"A"}]},{"text":"So I have the workflow working here and it does Ubuntu, it does Windows, it does Android. So all that stuff is available to you. I would never recommend using Miskit on an Apple platform for obvious reasons, like what's the point? True. Unless there's something special that I provide that CloudKit doesn't like, I don't get it.","start":3038790,"end":3060320,"confidence":0.94628906,"words":[{"text":"So","start":3038790,"end":3039110,"confidence":0.94628906,"speaker":"A"},{"text":"I","start":3039110,"end":3039350,"confidence":0.9995117,"speaker":"A"},{"text":"have","start":3039350,"end":3039630,"confidence":1,"speaker":"A"},{"text":"the","start":3039630,"end":3039870,"confidence":0.9980469,"speaker":"A"},{"text":"workflow","start":3039870,"end":3040350,"confidence":0.9995117,"speaker":"A"},{"text":"working","start":3040350,"end":3040630,"confidence":0.9995117,"speaker":"A"},{"text":"here","start":3041190,"end":3041590,"confidence":0.99853516,"speaker":"A"},{"text":"and","start":3041670,"end":3041950,"confidence":0.9892578,"speaker":"A"},{"text":"it","start":3041950,"end":3042070,"confidence":0.9995117,"speaker":"A"},{"text":"does","start":3042070,"end":3042270,"confidence":0.99902344,"speaker":"A"},{"text":"Ubuntu,","start":3042270,"end":3043110,"confidence":0.9856445,"speaker":"A"},{"text":"it","start":3044080,"end":3044200,"confidence":0.97216797,"speaker":"A"},{"text":"does","start":3044200,"end":3044400,"confidence":0.99853516,"speaker":"A"},{"text":"Windows,","start":3044400,"end":3044960,"confidence":0.9944661,"speaker":"A"},{"text":"it","start":3045120,"end":3045400,"confidence":0.99365234,"speaker":"A"},{"text":"does","start":3045400,"end":3045600,"confidence":0.98779297,"speaker":"A"},{"text":"Android.","start":3045600,"end":3046120,"confidence":0.9943034,"speaker":"A"},{"text":"So","start":3046120,"end":3046360,"confidence":0.98046875,"speaker":"A"},{"text":"all","start":3046360,"end":3046480,"confidence":0.99853516,"speaker":"A"},{"text":"that","start":3046480,"end":3046600,"confidence":0.9975586,"speaker":"A"},{"text":"stuff","start":3046600,"end":3046880,"confidence":0.90494794,"speaker":"A"},{"text":"is","start":3046880,"end":3047080,"confidence":0.9995117,"speaker":"A"},{"text":"available","start":3047080,"end":3047360,"confidence":0.9995117,"speaker":"A"},{"text":"to","start":3047440,"end":3047720,"confidence":0.99902344,"speaker":"A"},{"text":"you.","start":3047720,"end":3048000,"confidence":0.99853516,"speaker":"A"},{"text":"I","start":3048640,"end":3048960,"confidence":0.9995117,"speaker":"A"},{"text":"would","start":3048960,"end":3049200,"confidence":0.9995117,"speaker":"A"},{"text":"never","start":3049200,"end":3049440,"confidence":1,"speaker":"A"},{"text":"recommend","start":3049440,"end":3049920,"confidence":0.9998372,"speaker":"A"},{"text":"using","start":3049920,"end":3050240,"confidence":0.99902344,"speaker":"A"},{"text":"Miskit","start":3050240,"end":3050920,"confidence":0.9777832,"speaker":"A"},{"text":"on","start":3050920,"end":3051160,"confidence":0.99902344,"speaker":"A"},{"text":"an","start":3051160,"end":3051320,"confidence":0.99902344,"speaker":"A"},{"text":"Apple","start":3051320,"end":3051560,"confidence":1,"speaker":"A"},{"text":"platform","start":3051560,"end":3052040,"confidence":0.9992676,"speaker":"A"},{"text":"for","start":3052040,"end":3052280,"confidence":0.9995117,"speaker":"A"},{"text":"obvious","start":3052280,"end":3052640,"confidence":0.99975586,"speaker":"A"},{"text":"reasons,","start":3052640,"end":3053200,"confidence":0.9995117,"speaker":"A"},{"text":"like","start":3053280,"end":3053600,"confidence":0.9238281,"speaker":"A"},{"text":"what's","start":3053600,"end":3053840,"confidence":0.9995117,"speaker":"A"},{"text":"the","start":3053840,"end":3053960,"confidence":0.9995117,"speaker":"A"},{"text":"point?","start":3053960,"end":3054240,"confidence":0.99902344,"speaker":"A"},{"text":"True.","start":3055600,"end":3056080,"confidence":0.9099121,"speaker":"C"},{"text":"Unless","start":3056080,"end":3056440,"confidence":0.99609375,"speaker":"A"},{"text":"there's","start":3056440,"end":3056720,"confidence":0.9946289,"speaker":"A"},{"text":"something","start":3056720,"end":3056920,"confidence":1,"speaker":"A"},{"text":"special","start":3056920,"end":3057240,"confidence":1,"speaker":"A"},{"text":"that","start":3057240,"end":3057480,"confidence":0.9970703,"speaker":"A"},{"text":"I","start":3057480,"end":3057640,"confidence":0.9995117,"speaker":"A"},{"text":"provide","start":3057640,"end":3057880,"confidence":1,"speaker":"A"},{"text":"that","start":3057880,"end":3058160,"confidence":0.9897461,"speaker":"A"},{"text":"CloudKit","start":3058160,"end":3058760,"confidence":0.89551,"speaker":"A"},{"text":"doesn't","start":3058760,"end":3059040,"confidence":0.96777344,"speaker":"A"},{"text":"like,","start":3059040,"end":3059360,"confidence":0.83496094,"speaker":"A"},{"text":"I","start":3059440,"end":3059680,"confidence":0.99560547,"speaker":"A"},{"text":"don't","start":3059680,"end":3059920,"confidence":0.8590495,"speaker":"A"},{"text":"get","start":3059920,"end":3060039,"confidence":0.9995117,"speaker":"A"},{"text":"it.","start":3060039,"end":3060320,"confidence":0.9980469,"speaker":"A"}]},{"text":"Right. But we have an issue. So I just started dabbling. I haven't really done anything with wasm, but I did definitely try. Like I added support for WASM in my, in my Swift build action.","start":3060480,"end":3074890,"confidence":0.8925781,"words":[{"text":"Right.","start":3060480,"end":3060880,"confidence":0.8925781,"speaker":"C"},{"text":"But","start":3061200,"end":3061600,"confidence":0.9941406,"speaker":"A"},{"text":"we","start":3062560,"end":3062880,"confidence":0.9926758,"speaker":"A"},{"text":"have","start":3062880,"end":3063200,"confidence":0.9995117,"speaker":"A"},{"text":"an","start":3063200,"end":3063520,"confidence":0.9770508,"speaker":"A"},{"text":"issue.","start":3063520,"end":3063840,"confidence":0.9765625,"speaker":"A"},{"text":"So","start":3063920,"end":3064200,"confidence":0.9794922,"speaker":"A"},{"text":"I","start":3064200,"end":3064360,"confidence":0.9995117,"speaker":"A"},{"text":"just","start":3064360,"end":3064560,"confidence":0.99902344,"speaker":"A"},{"text":"started","start":3064560,"end":3064840,"confidence":0.9995117,"speaker":"A"},{"text":"dabbling.","start":3064840,"end":3065440,"confidence":0.91918945,"speaker":"A"},{"text":"I","start":3066000,"end":3066280,"confidence":0.609375,"speaker":"A"},{"text":"haven't","start":3066280,"end":3066520,"confidence":0.9489746,"speaker":"A"},{"text":"really","start":3066520,"end":3066800,"confidence":0.9975586,"speaker":"A"},{"text":"done","start":3066960,"end":3067280,"confidence":1,"speaker":"A"},{"text":"anything","start":3067280,"end":3067640,"confidence":1,"speaker":"A"},{"text":"with","start":3067640,"end":3067840,"confidence":0.9995117,"speaker":"A"},{"text":"wasm,","start":3067840,"end":3068480,"confidence":0.6376953,"speaker":"A"},{"text":"but","start":3069450,"end":3069530,"confidence":0.9980469,"speaker":"A"},{"text":"I","start":3069530,"end":3069650,"confidence":0.9980469,"speaker":"A"},{"text":"did","start":3069650,"end":3069810,"confidence":0.99853516,"speaker":"A"},{"text":"definitely","start":3069810,"end":3070210,"confidence":0.83239746,"speaker":"A"},{"text":"try.","start":3070210,"end":3070570,"confidence":0.99902344,"speaker":"A"},{"text":"Like","start":3070570,"end":3070850,"confidence":0.9980469,"speaker":"A"},{"text":"I","start":3070850,"end":3071010,"confidence":0.99609375,"speaker":"A"},{"text":"added","start":3071010,"end":3071250,"confidence":0.99902344,"speaker":"A"},{"text":"support","start":3071250,"end":3071530,"confidence":0.99853516,"speaker":"A"},{"text":"for","start":3071530,"end":3071730,"confidence":0.99853516,"speaker":"A"},{"text":"WASM","start":3071730,"end":3072250,"confidence":0.5599365,"speaker":"A"},{"text":"in","start":3072250,"end":3072450,"confidence":0.9560547,"speaker":"A"},{"text":"my,","start":3072450,"end":3072730,"confidence":0.9975586,"speaker":"A"},{"text":"in","start":3072730,"end":3073050,"confidence":0.9980469,"speaker":"A"},{"text":"my","start":3073050,"end":3073370,"confidence":1,"speaker":"A"},{"text":"Swift","start":3073690,"end":3074210,"confidence":0.9980469,"speaker":"A"},{"text":"build","start":3074210,"end":3074530,"confidence":0.99609375,"speaker":"A"},{"text":"action.","start":3074530,"end":3074890,"confidence":0.99902344,"speaker":"A"}]},{"text":"The thing about WASA is it does not provide. It doesn't have a transport available. So we talked about transports, I think. Did you hear about that part about the Open API generator and transports? I think I was coming in at that point.","start":3077210,"end":3093690,"confidence":0.99121094,"words":[{"text":"The","start":3077210,"end":3077490,"confidence":0.99121094,"speaker":"A"},{"text":"thing","start":3077490,"end":3077650,"confidence":0.9980469,"speaker":"A"},{"text":"about","start":3077650,"end":3077930,"confidence":0.9995117,"speaker":"A"},{"text":"WASA","start":3077930,"end":3078650,"confidence":0.66918945,"speaker":"A"},{"text":"is","start":3078650,"end":3078850,"confidence":0.9995117,"speaker":"A"},{"text":"it","start":3078850,"end":3079010,"confidence":0.99853516,"speaker":"A"},{"text":"does","start":3079010,"end":3079210,"confidence":0.99853516,"speaker":"A"},{"text":"not","start":3079210,"end":3079410,"confidence":0.99560547,"speaker":"A"},{"text":"provide.","start":3079410,"end":3079690,"confidence":0.99902344,"speaker":"A"},{"text":"It","start":3079770,"end":3080050,"confidence":0.99609375,"speaker":"A"},{"text":"doesn't","start":3080050,"end":3080290,"confidence":0.9978841,"speaker":"A"},{"text":"have","start":3080290,"end":3080410,"confidence":1,"speaker":"A"},{"text":"a","start":3080410,"end":3080530,"confidence":0.99853516,"speaker":"A"},{"text":"transport","start":3080530,"end":3081050,"confidence":0.99853516,"speaker":"A"},{"text":"available.","start":3081130,"end":3081530,"confidence":0.9995117,"speaker":"A"},{"text":"So","start":3082570,"end":3082850,"confidence":0.99853516,"speaker":"A"},{"text":"we","start":3082850,"end":3083050,"confidence":0.99853516,"speaker":"A"},{"text":"talked","start":3083050,"end":3083290,"confidence":0.99975586,"speaker":"A"},{"text":"about","start":3083290,"end":3083490,"confidence":0.9995117,"speaker":"A"},{"text":"transports,","start":3083490,"end":3084410,"confidence":0.9938151,"speaker":"A"},{"text":"I","start":3086010,"end":3086250,"confidence":0.9770508,"speaker":"A"},{"text":"think.","start":3086250,"end":3086490,"confidence":0.9980469,"speaker":"A"},{"text":"Did","start":3086570,"end":3086850,"confidence":0.99853516,"speaker":"A"},{"text":"you","start":3086850,"end":3087010,"confidence":1,"speaker":"A"},{"text":"hear","start":3087010,"end":3087170,"confidence":0.9995117,"speaker":"A"},{"text":"about","start":3087170,"end":3087330,"confidence":0.99902344,"speaker":"A"},{"text":"that","start":3087330,"end":3087530,"confidence":0.9970703,"speaker":"A"},{"text":"part","start":3087530,"end":3087770,"confidence":0.9995117,"speaker":"A"},{"text":"about","start":3087770,"end":3087970,"confidence":0.9995117,"speaker":"A"},{"text":"the","start":3087970,"end":3088090,"confidence":0.9995117,"speaker":"A"},{"text":"Open","start":3088090,"end":3088250,"confidence":0.99902344,"speaker":"A"},{"text":"API","start":3088250,"end":3088770,"confidence":0.7873535,"speaker":"A"},{"text":"generator","start":3088770,"end":3089170,"confidence":0.9995117,"speaker":"A"},{"text":"and","start":3089170,"end":3089330,"confidence":0.95751953,"speaker":"A"},{"text":"transports?","start":3089330,"end":3090090,"confidence":0.8383789,"speaker":"A"},{"text":"I","start":3091370,"end":3091770,"confidence":0.9667969,"speaker":"C"},{"text":"think","start":3091850,"end":3092170,"confidence":0.9995117,"speaker":"C"},{"text":"I","start":3092170,"end":3092370,"confidence":0.9970703,"speaker":"C"},{"text":"was","start":3092370,"end":3092570,"confidence":1,"speaker":"C"},{"text":"coming","start":3092570,"end":3092810,"confidence":0.9995117,"speaker":"C"},{"text":"in","start":3092810,"end":3093010,"confidence":0.9980469,"speaker":"C"},{"text":"at","start":3093010,"end":3093130,"confidence":1,"speaker":"C"},{"text":"that","start":3093130,"end":3093330,"confidence":0.99560547,"speaker":"C"},{"text":"point.","start":3093330,"end":3093690,"confidence":0.9980469,"speaker":"C"}]},{"text":"Okay. When you create a client, so underneath the client you have what's called a client transport. This is so underneath this client, this is an abstraction layer above. So this is not the right one. Where's the public one?","start":3094410,"end":3113390,"confidence":0.92496747,"words":[{"text":"Okay.","start":3094410,"end":3094920,"confidence":0.92496747,"speaker":"A"},{"text":"When","start":3095630,"end":3095750,"confidence":0.71191406,"speaker":"A"},{"text":"you","start":3095750,"end":3095910,"confidence":0.93408203,"speaker":"A"},{"text":"create","start":3095910,"end":3096070,"confidence":0.99853516,"speaker":"A"},{"text":"a","start":3096070,"end":3096230,"confidence":0.9951172,"speaker":"A"},{"text":"client,","start":3096230,"end":3096670,"confidence":0.9995117,"speaker":"A"},{"text":"so","start":3097630,"end":3097910,"confidence":0.9794922,"speaker":"A"},{"text":"underneath","start":3097910,"end":3098310,"confidence":0.9996745,"speaker":"A"},{"text":"the","start":3098310,"end":3098470,"confidence":0.9995117,"speaker":"A"},{"text":"client","start":3098470,"end":3098910,"confidence":1,"speaker":"A"},{"text":"you","start":3102350,"end":3102630,"confidence":0.99902344,"speaker":"A"},{"text":"have","start":3102630,"end":3102910,"confidence":1,"speaker":"A"},{"text":"what's","start":3102910,"end":3103230,"confidence":0.99934894,"speaker":"A"},{"text":"called","start":3103230,"end":3103350,"confidence":1,"speaker":"A"},{"text":"a","start":3103350,"end":3103510,"confidence":0.7114258,"speaker":"A"},{"text":"client","start":3103510,"end":3103790,"confidence":0.81811523,"speaker":"A"},{"text":"transport.","start":3103790,"end":3104430,"confidence":0.9987793,"speaker":"A"},{"text":"This","start":3104670,"end":3104950,"confidence":0.8666992,"speaker":"A"},{"text":"is","start":3104950,"end":3105230,"confidence":0.99902344,"speaker":"A"},{"text":"so","start":3105630,"end":3105910,"confidence":0.9921875,"speaker":"A"},{"text":"underneath","start":3105910,"end":3106430,"confidence":0.90999347,"speaker":"A"},{"text":"this","start":3106670,"end":3106990,"confidence":0.99902344,"speaker":"A"},{"text":"client,","start":3106990,"end":3107310,"confidence":0.9941406,"speaker":"A"},{"text":"this","start":3107310,"end":3107510,"confidence":0.99902344,"speaker":"A"},{"text":"is","start":3107510,"end":3107630,"confidence":0.9995117,"speaker":"A"},{"text":"an","start":3107630,"end":3107750,"confidence":0.99902344,"speaker":"A"},{"text":"abstraction","start":3107750,"end":3108350,"confidence":0.99975586,"speaker":"A"},{"text":"layer","start":3108350,"end":3108750,"confidence":0.9995117,"speaker":"A"},{"text":"above.","start":3108750,"end":3109150,"confidence":0.8647461,"speaker":"A"},{"text":"So","start":3109870,"end":3110190,"confidence":0.58496094,"speaker":"A"},{"text":"this","start":3110190,"end":3110390,"confidence":0.9995117,"speaker":"A"},{"text":"is","start":3110390,"end":3110550,"confidence":0.99902344,"speaker":"A"},{"text":"not","start":3110550,"end":3110829,"confidence":0.9921875,"speaker":"A"},{"text":"the","start":3110829,"end":3111109,"confidence":0.9995117,"speaker":"A"},{"text":"right","start":3111109,"end":3111270,"confidence":0.99609375,"speaker":"A"},{"text":"one.","start":3111270,"end":3111550,"confidence":0.98339844,"speaker":"A"},{"text":"Where's","start":3112190,"end":3112630,"confidence":0.98323566,"speaker":"A"},{"text":"the","start":3112630,"end":3112790,"confidence":1,"speaker":"A"},{"text":"public","start":3112790,"end":3113030,"confidence":0.9995117,"speaker":"A"},{"text":"one?","start":3113030,"end":3113390,"confidence":0.9916992,"speaker":"A"}]},{"text":"But anyway, there is here CloudKit service maybe.","start":3120680,"end":3126920,"confidence":0.99560547,"words":[{"text":"But","start":3120680,"end":3120800,"confidence":0.99560547,"speaker":"A"},{"text":"anyway,","start":3120800,"end":3121160,"confidence":0.9995117,"speaker":"A"},{"text":"there","start":3121160,"end":3121400,"confidence":0.9980469,"speaker":"A"},{"text":"is","start":3121400,"end":3121720,"confidence":0.9995117,"speaker":"A"},{"text":"here","start":3125080,"end":3125440,"confidence":0.97509766,"speaker":"A"},{"text":"CloudKit","start":3125440,"end":3126040,"confidence":0.98950195,"speaker":"A"},{"text":"service","start":3126040,"end":3126360,"confidence":0.9975586,"speaker":"A"},{"text":"maybe.","start":3126360,"end":3126920,"confidence":0.9958496,"speaker":"A"}]},{"text":"Yeah, here we go. So the CloudKit service has a client and part of the client is being able to say what transport you use in Open API. And there's two transports available right now. One is, one is your regular URL session for clients, which. That makes sense.","start":3129560,"end":3160930,"confidence":0.87158203,"words":[{"text":"Yeah,","start":3129560,"end":3129920,"confidence":0.87158203,"speaker":"A"},{"text":"here","start":3129920,"end":3130080,"confidence":0.99853516,"speaker":"A"},{"text":"we","start":3130080,"end":3130240,"confidence":1,"speaker":"A"},{"text":"go.","start":3130240,"end":3130520,"confidence":1,"speaker":"A"},{"text":"So","start":3131320,"end":3131560,"confidence":0.9921875,"speaker":"A"},{"text":"the","start":3131560,"end":3131640,"confidence":0.9995117,"speaker":"A"},{"text":"CloudKit","start":3131640,"end":3132280,"confidence":0.9147949,"speaker":"A"},{"text":"service","start":3132440,"end":3132840,"confidence":0.99609375,"speaker":"A"},{"text":"has","start":3133320,"end":3133640,"confidence":1,"speaker":"A"},{"text":"a","start":3133640,"end":3133840,"confidence":0.9995117,"speaker":"A"},{"text":"client","start":3133840,"end":3134360,"confidence":0.99975586,"speaker":"A"},{"text":"and","start":3135320,"end":3135640,"confidence":0.984375,"speaker":"A"},{"text":"part","start":3135640,"end":3135840,"confidence":1,"speaker":"A"},{"text":"of","start":3135840,"end":3136000,"confidence":1,"speaker":"A"},{"text":"the","start":3136000,"end":3136160,"confidence":1,"speaker":"A"},{"text":"client","start":3136160,"end":3136600,"confidence":0.99975586,"speaker":"A"},{"text":"is","start":3136920,"end":3137240,"confidence":0.99658203,"speaker":"A"},{"text":"being","start":3137240,"end":3137560,"confidence":0.9995117,"speaker":"A"},{"text":"able","start":3137560,"end":3137960,"confidence":0.9995117,"speaker":"A"},{"text":"to","start":3139960,"end":3140360,"confidence":1,"speaker":"A"},{"text":"say","start":3140440,"end":3140760,"confidence":0.9951172,"speaker":"A"},{"text":"what","start":3140760,"end":3140960,"confidence":0.9975586,"speaker":"A"},{"text":"transport","start":3140960,"end":3141520,"confidence":0.99853516,"speaker":"A"},{"text":"you","start":3141520,"end":3141760,"confidence":0.99609375,"speaker":"A"},{"text":"use","start":3141760,"end":3142040,"confidence":0.9970703,"speaker":"A"},{"text":"in","start":3142360,"end":3142640,"confidence":0.9169922,"speaker":"A"},{"text":"Open","start":3142640,"end":3142840,"confidence":0.9995117,"speaker":"A"},{"text":"API.","start":3142840,"end":3143560,"confidence":0.7491455,"speaker":"A"},{"text":"And","start":3144760,"end":3145160,"confidence":0.9868164,"speaker":"A"},{"text":"there's","start":3148850,"end":3149330,"confidence":0.84521484,"speaker":"A"},{"text":"two","start":3149330,"end":3149650,"confidence":0.99609375,"speaker":"A"},{"text":"transports","start":3149970,"end":3150730,"confidence":0.9951172,"speaker":"A"},{"text":"available","start":3150730,"end":3151010,"confidence":0.9995117,"speaker":"A"},{"text":"right","start":3151010,"end":3151330,"confidence":0.9995117,"speaker":"A"},{"text":"now.","start":3151330,"end":3151650,"confidence":0.9970703,"speaker":"A"},{"text":"One","start":3152770,"end":3153170,"confidence":0.9663086,"speaker":"A"},{"text":"is,","start":3153330,"end":3153730,"confidence":0.9975586,"speaker":"A"},{"text":"one","start":3156850,"end":3157170,"confidence":0.9892578,"speaker":"A"},{"text":"is","start":3157170,"end":3157490,"confidence":0.99853516,"speaker":"A"},{"text":"your","start":3157490,"end":3157810,"confidence":0.99658203,"speaker":"A"},{"text":"regular","start":3157810,"end":3158210,"confidence":1,"speaker":"A"},{"text":"URL","start":3158210,"end":3158770,"confidence":0.9992676,"speaker":"A"},{"text":"session","start":3158770,"end":3159130,"confidence":0.99934894,"speaker":"A"},{"text":"for","start":3159130,"end":3159290,"confidence":0.99853516,"speaker":"A"},{"text":"clients,","start":3159290,"end":3159730,"confidence":0.78100586,"speaker":"A"},{"text":"which.","start":3159890,"end":3160210,"confidence":0.99853516,"speaker":"A"},{"text":"That","start":3160210,"end":3160410,"confidence":0.9916992,"speaker":"A"},{"text":"makes","start":3160410,"end":3160610,"confidence":0.9951172,"speaker":"A"},{"text":"sense.","start":3160610,"end":3160930,"confidence":0.9995117,"speaker":"A"}]},{"text":"Right. And then there's the Async HTTP client which is typically used like Swift NEO based for servers. The thing is that neither of those are available in wasp. Do you know what WASM is? I have no experience with it, but yes.","start":3160930,"end":3177810,"confidence":0.9897461,"words":[{"text":"Right.","start":3160930,"end":3161250,"confidence":0.9897461,"speaker":"A"},{"text":"And","start":3161570,"end":3161890,"confidence":0.9921875,"speaker":"A"},{"text":"then","start":3161890,"end":3162089,"confidence":0.9892578,"speaker":"A"},{"text":"there's","start":3162089,"end":3162410,"confidence":0.9840495,"speaker":"A"},{"text":"the","start":3162410,"end":3162570,"confidence":0.9584961,"speaker":"A"},{"text":"Async","start":3162570,"end":3163170,"confidence":0.9949951,"speaker":"A"},{"text":"HTTP","start":3163170,"end":3163810,"confidence":0.9881592,"speaker":"A"},{"text":"client","start":3163810,"end":3164170,"confidence":0.9968262,"speaker":"A"},{"text":"which","start":3164170,"end":3164410,"confidence":0.9995117,"speaker":"A"},{"text":"is","start":3164410,"end":3164690,"confidence":0.9995117,"speaker":"A"},{"text":"typically","start":3164690,"end":3165090,"confidence":0.99975586,"speaker":"A"},{"text":"used","start":3165090,"end":3165410,"confidence":0.99658203,"speaker":"A"},{"text":"like","start":3165570,"end":3165850,"confidence":0.9838867,"speaker":"A"},{"text":"Swift","start":3165850,"end":3166130,"confidence":0.89575195,"speaker":"A"},{"text":"NEO","start":3166130,"end":3166530,"confidence":0.94506836,"speaker":"A"},{"text":"based","start":3166530,"end":3166850,"confidence":0.9980469,"speaker":"A"},{"text":"for","start":3167170,"end":3167490,"confidence":0.99560547,"speaker":"A"},{"text":"servers.","start":3167490,"end":3167970,"confidence":0.90649414,"speaker":"A"},{"text":"The","start":3169330,"end":3169610,"confidence":0.99853516,"speaker":"A"},{"text":"thing","start":3169610,"end":3169770,"confidence":0.9995117,"speaker":"A"},{"text":"is","start":3169770,"end":3169970,"confidence":0.9995117,"speaker":"A"},{"text":"that","start":3169970,"end":3170170,"confidence":0.52441406,"speaker":"A"},{"text":"neither","start":3170170,"end":3170410,"confidence":0.99902344,"speaker":"A"},{"text":"of","start":3170410,"end":3170530,"confidence":0.9916992,"speaker":"A"},{"text":"those","start":3170530,"end":3170770,"confidence":0.9980469,"speaker":"A"},{"text":"are","start":3170930,"end":3171250,"confidence":0.99902344,"speaker":"A"},{"text":"available","start":3171250,"end":3171570,"confidence":0.99365234,"speaker":"A"},{"text":"in","start":3171730,"end":3172130,"confidence":0.9638672,"speaker":"A"},{"text":"wasp.","start":3172610,"end":3173170,"confidence":0.58813477,"speaker":"A"},{"text":"Do","start":3174290,"end":3174530,"confidence":0.6435547,"speaker":"A"},{"text":"you","start":3174530,"end":3174610,"confidence":0.99853516,"speaker":"A"},{"text":"know","start":3174610,"end":3174690,"confidence":0.9995117,"speaker":"A"},{"text":"what","start":3174690,"end":3174810,"confidence":0.9980469,"speaker":"A"},{"text":"WASM","start":3174810,"end":3175210,"confidence":0.78027344,"speaker":"A"},{"text":"is?","start":3175210,"end":3175490,"confidence":0.99853516,"speaker":"A"},{"text":"I","start":3176050,"end":3176290,"confidence":0.99902344,"speaker":"C"},{"text":"have","start":3176290,"end":3176410,"confidence":0.9995117,"speaker":"C"},{"text":"no","start":3176410,"end":3176570,"confidence":1,"speaker":"C"},{"text":"experience","start":3176570,"end":3176850,"confidence":1,"speaker":"C"},{"text":"with","start":3176850,"end":3177130,"confidence":0.9995117,"speaker":"C"},{"text":"it,","start":3177130,"end":3177290,"confidence":0.99853516,"speaker":"C"},{"text":"but","start":3177290,"end":3177450,"confidence":0.8720703,"speaker":"C"},{"text":"yes.","start":3177450,"end":3177810,"confidence":0.9963379,"speaker":"C"}]},{"text":"Okay. It's. It's the web browser. Right. So.","start":3178850,"end":3182290,"confidence":0.9892578,"words":[{"text":"Okay.","start":3178850,"end":3179410,"confidence":0.9892578,"speaker":"A"},{"text":"It's.","start":3179490,"end":3179850,"confidence":0.96240234,"speaker":"A"},{"text":"It's","start":3179850,"end":3180290,"confidence":0.98811847,"speaker":"A"},{"text":"the","start":3180290,"end":3180570,"confidence":1,"speaker":"A"},{"text":"web","start":3180570,"end":3180810,"confidence":1,"speaker":"A"},{"text":"browser.","start":3180810,"end":3181210,"confidence":0.99869794,"speaker":"A"},{"text":"Right.","start":3181210,"end":3181490,"confidence":0.99853516,"speaker":"A"},{"text":"So.","start":3181890,"end":3182290,"confidence":0.98876953,"speaker":"A"}]},{"text":"So you really can't use Miskit in. In the. In WASM yet because there is no transport. Now having said that, why on earth would you use. Awesome.","start":3182690,"end":3193810,"confidence":0.9975586,"words":[{"text":"So","start":3182690,"end":3182970,"confidence":0.9975586,"speaker":"A"},{"text":"you","start":3182970,"end":3183130,"confidence":1,"speaker":"A"},{"text":"really","start":3183130,"end":3183290,"confidence":1,"speaker":"A"},{"text":"can't","start":3183290,"end":3183490,"confidence":0.9998372,"speaker":"A"},{"text":"use","start":3183490,"end":3183690,"confidence":0.9995117,"speaker":"A"},{"text":"Miskit","start":3183690,"end":3184370,"confidence":0.95788574,"speaker":"A"},{"text":"in.","start":3184450,"end":3184850,"confidence":0.921875,"speaker":"A"},{"text":"In","start":3186450,"end":3186730,"confidence":0.99609375,"speaker":"A"},{"text":"the.","start":3186730,"end":3186930,"confidence":0.99609375,"speaker":"A"},{"text":"In","start":3186930,"end":3187170,"confidence":0.99658203,"speaker":"A"},{"text":"WASM","start":3187170,"end":3187690,"confidence":0.7368164,"speaker":"A"},{"text":"yet","start":3187690,"end":3187890,"confidence":0.85009766,"speaker":"A"},{"text":"because","start":3187890,"end":3188090,"confidence":1,"speaker":"A"},{"text":"there","start":3188090,"end":3188250,"confidence":1,"speaker":"A"},{"text":"is","start":3188250,"end":3188450,"confidence":0.9975586,"speaker":"A"},{"text":"no","start":3188450,"end":3188649,"confidence":0.9995117,"speaker":"A"},{"text":"transport.","start":3188649,"end":3189170,"confidence":0.998291,"speaker":"A"},{"text":"Now","start":3189170,"end":3189450,"confidence":0.9995117,"speaker":"A"},{"text":"having","start":3189450,"end":3189650,"confidence":1,"speaker":"A"},{"text":"said","start":3189650,"end":3189890,"confidence":1,"speaker":"A"},{"text":"that,","start":3189890,"end":3190210,"confidence":1,"speaker":"A"},{"text":"why","start":3190530,"end":3190850,"confidence":0.99902344,"speaker":"A"},{"text":"on","start":3190850,"end":3191050,"confidence":0.99902344,"speaker":"A"},{"text":"earth","start":3191050,"end":3191290,"confidence":1,"speaker":"A"},{"text":"would","start":3191290,"end":3191450,"confidence":0.9995117,"speaker":"A"},{"text":"you","start":3191450,"end":3191730,"confidence":0.9995117,"speaker":"A"},{"text":"use.","start":3192050,"end":3192450,"confidence":0.99658203,"speaker":"A"},{"text":"Awesome.","start":3193090,"end":3193810,"confidence":0.7972819,"speaker":"A"}]},{"text":"Why would you use Miskit in the browser? Why not just use CloudKit js? So that's essentially, you know, What other questions do you have?","start":3194050,"end":3210940,"confidence":0.7753906,"words":[{"text":"Why","start":3194050,"end":3194330,"confidence":0.7753906,"speaker":"A"},{"text":"would","start":3194330,"end":3194450,"confidence":0.9667969,"speaker":"A"},{"text":"you","start":3194450,"end":3194530,"confidence":0.8652344,"speaker":"A"},{"text":"use","start":3194530,"end":3194650,"confidence":0.9169922,"speaker":"A"},{"text":"Miskit","start":3194650,"end":3195130,"confidence":0.9088135,"speaker":"A"},{"text":"in","start":3195130,"end":3195250,"confidence":0.99609375,"speaker":"A"},{"text":"the","start":3195250,"end":3195330,"confidence":0.9995117,"speaker":"A"},{"text":"browser?","start":3195330,"end":3195690,"confidence":1,"speaker":"A"},{"text":"Why","start":3195690,"end":3195930,"confidence":0.9995117,"speaker":"A"},{"text":"not","start":3195930,"end":3196090,"confidence":0.9995117,"speaker":"A"},{"text":"just","start":3196090,"end":3196250,"confidence":0.9995117,"speaker":"A"},{"text":"use","start":3196250,"end":3196450,"confidence":0.9995117,"speaker":"A"},{"text":"CloudKit","start":3196450,"end":3196970,"confidence":0.99780273,"speaker":"A"},{"text":"js?","start":3196970,"end":3197410,"confidence":0.8005371,"speaker":"A"},{"text":"So","start":3198380,"end":3198620,"confidence":0.98828125,"speaker":"A"},{"text":"that's","start":3199660,"end":3200100,"confidence":0.9996745,"speaker":"A"},{"text":"essentially,","start":3200100,"end":3200700,"confidence":0.9996338,"speaker":"A"},{"text":"you","start":3201580,"end":3201820,"confidence":0.765625,"speaker":"A"},{"text":"know,","start":3201820,"end":3202060,"confidence":0.77685547,"speaker":"A"},{"text":"What","start":3209260,"end":3209540,"confidence":0.99902344,"speaker":"A"},{"text":"other","start":3209540,"end":3209780,"confidence":0.9975586,"speaker":"A"},{"text":"questions","start":3209780,"end":3210340,"confidence":0.99975586,"speaker":"A"},{"text":"do","start":3210340,"end":3210500,"confidence":0.9995117,"speaker":"A"},{"text":"you","start":3210500,"end":3210660,"confidence":1,"speaker":"A"},{"text":"have?","start":3210660,"end":3210940,"confidence":1,"speaker":"A"}]},{"text":"My brain is mushy right now, so.","start":3215660,"end":3218300,"confidence":0.96240234,"words":[{"text":"My","start":3215660,"end":3216060,"confidence":0.96240234,"speaker":"C"},{"text":"brain","start":3216300,"end":3216780,"confidence":0.99975586,"speaker":"C"},{"text":"is","start":3216780,"end":3217020,"confidence":0.9995117,"speaker":"C"},{"text":"mushy","start":3217020,"end":3217460,"confidence":0.9998372,"speaker":"C"},{"text":"right","start":3217460,"end":3217620,"confidence":0.9995117,"speaker":"C"},{"text":"now,","start":3217620,"end":3217900,"confidence":1,"speaker":"C"},{"text":"so.","start":3217900,"end":3218300,"confidence":0.9770508,"speaker":"C"}]},{"text":"Because of my presentation or because other. Things, I got two hours of sleep. Oh, I'm so sorry. So I'm following as best as I can.","start":3221020,"end":3231450,"confidence":0.9970703,"words":[{"text":"Because","start":3221020,"end":3221340,"confidence":0.9970703,"speaker":"A"},{"text":"of","start":3221340,"end":3221540,"confidence":0.99609375,"speaker":"A"},{"text":"my","start":3221540,"end":3221700,"confidence":0.99853516,"speaker":"A"},{"text":"presentation","start":3221700,"end":3222300,"confidence":0.99975586,"speaker":"A"},{"text":"or","start":3222300,"end":3222540,"confidence":0.9902344,"speaker":"A"},{"text":"because","start":3222540,"end":3222860,"confidence":0.99853516,"speaker":"A"},{"text":"other.","start":3223020,"end":3223380,"confidence":0.99902344,"speaker":"A"},{"text":"Things,","start":3223380,"end":3223740,"confidence":0.9946289,"speaker":"C"},{"text":"I","start":3224570,"end":3224730,"confidence":0.98876953,"speaker":"C"},{"text":"got","start":3224730,"end":3224930,"confidence":0.9995117,"speaker":"C"},{"text":"two","start":3224930,"end":3225090,"confidence":0.9995117,"speaker":"C"},{"text":"hours","start":3225090,"end":3225290,"confidence":1,"speaker":"C"},{"text":"of","start":3225290,"end":3225450,"confidence":0.9873047,"speaker":"C"},{"text":"sleep.","start":3225450,"end":3225850,"confidence":0.9555664,"speaker":"C"},{"text":"Oh,","start":3226650,"end":3226970,"confidence":0.7734375,"speaker":"A"},{"text":"I'm","start":3226970,"end":3227130,"confidence":0.9970703,"speaker":"A"},{"text":"so","start":3227130,"end":3227290,"confidence":0.99365234,"speaker":"A"},{"text":"sorry.","start":3227290,"end":3227690,"confidence":0.9998372,"speaker":"A"},{"text":"So","start":3228170,"end":3228570,"confidence":0.95214844,"speaker":"C"},{"text":"I'm","start":3229770,"end":3230170,"confidence":0.97526044,"speaker":"C"},{"text":"following","start":3230170,"end":3230450,"confidence":0.99853516,"speaker":"C"},{"text":"as","start":3230450,"end":3230690,"confidence":0.9995117,"speaker":"C"},{"text":"best","start":3230690,"end":3230850,"confidence":0.9980469,"speaker":"C"},{"text":"as","start":3230850,"end":3231010,"confidence":0.9941406,"speaker":"C"},{"text":"I","start":3231010,"end":3231170,"confidence":0.9995117,"speaker":"C"},{"text":"can.","start":3231170,"end":3231450,"confidence":0.99902344,"speaker":"C"}]},{"text":"Snuggling. Yeah, the intro was basically how I originally built it for hard Twitch in 2020 for a private database login for the Apple Watch because I don't want to have a login screen. And so basically there's a way in the web browser to link your Apple Watch to your account and then from there you don't need to authenticate anymore. Nice. I built that all from hand and then in 23 they came out with the Open API generator which was like, oh wait, what if I can create an open API file out of Apple's 10 year old documentation?","start":3234330,"end":3270800,"confidence":0.87927246,"words":[{"text":"Snuggling.","start":3234330,"end":3235050,"confidence":0.87927246,"speaker":"A"},{"text":"Yeah,","start":3237050,"end":3237410,"confidence":0.96761066,"speaker":"A"},{"text":"the","start":3237410,"end":3237570,"confidence":0.99609375,"speaker":"A"},{"text":"intro","start":3237570,"end":3238010,"confidence":0.99975586,"speaker":"A"},{"text":"was","start":3238090,"end":3238410,"confidence":0.99853516,"speaker":"A"},{"text":"basically","start":3238410,"end":3238890,"confidence":0.9995117,"speaker":"A"},{"text":"how","start":3239290,"end":3239610,"confidence":0.99902344,"speaker":"A"},{"text":"I","start":3239610,"end":3239930,"confidence":0.9946289,"speaker":"A"},{"text":"originally","start":3240490,"end":3241010,"confidence":0.9998372,"speaker":"A"},{"text":"built","start":3241010,"end":3241250,"confidence":0.992513,"speaker":"A"},{"text":"it","start":3241250,"end":3241410,"confidence":0.9814453,"speaker":"A"},{"text":"for","start":3241410,"end":3241570,"confidence":0.9995117,"speaker":"A"},{"text":"hard","start":3241570,"end":3241730,"confidence":0.4362793,"speaker":"A"},{"text":"Twitch","start":3241730,"end":3242050,"confidence":0.9111328,"speaker":"A"},{"text":"in","start":3242050,"end":3242210,"confidence":0.99316406,"speaker":"A"},{"text":"2020","start":3242210,"end":3242810,"confidence":0.99854,"speaker":"A"},{"text":"for","start":3243210,"end":3243490,"confidence":0.94628906,"speaker":"A"},{"text":"a","start":3243490,"end":3243650,"confidence":0.7871094,"speaker":"A"},{"text":"private","start":3243650,"end":3243890,"confidence":1,"speaker":"A"},{"text":"database","start":3243890,"end":3244570,"confidence":0.99576825,"speaker":"A"},{"text":"login","start":3244730,"end":3245450,"confidence":0.9367676,"speaker":"A"},{"text":"for","start":3245930,"end":3246210,"confidence":0.99902344,"speaker":"A"},{"text":"the","start":3246210,"end":3246370,"confidence":0.9980469,"speaker":"A"},{"text":"Apple","start":3246370,"end":3246650,"confidence":0.99975586,"speaker":"A"},{"text":"Watch","start":3246650,"end":3246890,"confidence":0.8803711,"speaker":"A"},{"text":"because","start":3246890,"end":3247170,"confidence":0.99853516,"speaker":"A"},{"text":"I","start":3247170,"end":3247290,"confidence":0.9975586,"speaker":"A"},{"text":"don't","start":3247290,"end":3247450,"confidence":0.99658203,"speaker":"A"},{"text":"want","start":3247450,"end":3247530,"confidence":0.8720703,"speaker":"A"},{"text":"to","start":3247530,"end":3247610,"confidence":0.9980469,"speaker":"A"},{"text":"have","start":3247610,"end":3247690,"confidence":0.9995117,"speaker":"A"},{"text":"a","start":3247690,"end":3247810,"confidence":0.99853516,"speaker":"A"},{"text":"login","start":3247810,"end":3248210,"confidence":0.99731445,"speaker":"A"},{"text":"screen.","start":3248210,"end":3248490,"confidence":0.99975586,"speaker":"A"},{"text":"And","start":3248490,"end":3248690,"confidence":0.98583984,"speaker":"A"},{"text":"so","start":3248690,"end":3248810,"confidence":0.99902344,"speaker":"A"},{"text":"basically","start":3248810,"end":3249210,"confidence":0.9995117,"speaker":"A"},{"text":"there's","start":3249210,"end":3249570,"confidence":0.99934894,"speaker":"A"},{"text":"a","start":3249570,"end":3249690,"confidence":0.99853516,"speaker":"A"},{"text":"way","start":3249690,"end":3249810,"confidence":0.99853516,"speaker":"A"},{"text":"in","start":3249810,"end":3249930,"confidence":0.9980469,"speaker":"A"},{"text":"the","start":3249930,"end":3250010,"confidence":0.99902344,"speaker":"A"},{"text":"web","start":3250010,"end":3250170,"confidence":0.9995117,"speaker":"A"},{"text":"browser","start":3250170,"end":3250450,"confidence":1,"speaker":"A"},{"text":"to","start":3250450,"end":3250610,"confidence":0.99902344,"speaker":"A"},{"text":"link","start":3250610,"end":3250810,"confidence":0.99975586,"speaker":"A"},{"text":"your","start":3250810,"end":3250970,"confidence":0.99902344,"speaker":"A"},{"text":"Apple","start":3250970,"end":3251290,"confidence":0.9333496,"speaker":"A"},{"text":"Watch","start":3251290,"end":3251610,"confidence":0.9995117,"speaker":"A"},{"text":"to","start":3251770,"end":3252050,"confidence":0.9975586,"speaker":"A"},{"text":"your","start":3252050,"end":3252210,"confidence":0.99902344,"speaker":"A"},{"text":"account","start":3252210,"end":3252490,"confidence":1,"speaker":"A"},{"text":"and","start":3252490,"end":3252770,"confidence":0.99316406,"speaker":"A"},{"text":"then","start":3252770,"end":3252970,"confidence":0.8930664,"speaker":"A"},{"text":"from","start":3252970,"end":3253130,"confidence":1,"speaker":"A"},{"text":"there","start":3253130,"end":3253290,"confidence":1,"speaker":"A"},{"text":"you","start":3253290,"end":3253450,"confidence":1,"speaker":"A"},{"text":"don't","start":3253450,"end":3253610,"confidence":1,"speaker":"A"},{"text":"need","start":3253610,"end":3253730,"confidence":0.9995117,"speaker":"A"},{"text":"to","start":3253730,"end":3253850,"confidence":0.95947266,"speaker":"A"},{"text":"authenticate","start":3253850,"end":3254370,"confidence":0.99975586,"speaker":"A"},{"text":"anymore.","start":3254370,"end":3254890,"confidence":0.991862,"speaker":"A"},{"text":"Nice.","start":3255280,"end":3255600,"confidence":0.94921875,"speaker":"A"},{"text":"I","start":3255760,"end":3256000,"confidence":0.9970703,"speaker":"A"},{"text":"built","start":3256000,"end":3256280,"confidence":0.8284505,"speaker":"A"},{"text":"that","start":3256280,"end":3256440,"confidence":0.9692383,"speaker":"A"},{"text":"all","start":3256440,"end":3256600,"confidence":0.99609375,"speaker":"A"},{"text":"from","start":3256600,"end":3256800,"confidence":1,"speaker":"A"},{"text":"hand","start":3256800,"end":3257120,"confidence":0.9951172,"speaker":"A"},{"text":"and","start":3258400,"end":3258680,"confidence":0.73095703,"speaker":"A"},{"text":"then","start":3258680,"end":3258960,"confidence":0.9941406,"speaker":"A"},{"text":"in","start":3259200,"end":3259520,"confidence":0.9970703,"speaker":"A"},{"text":"23","start":3259520,"end":3260040,"confidence":0.9939,"speaker":"A"},{"text":"they","start":3260040,"end":3260280,"confidence":0.9995117,"speaker":"A"},{"text":"came","start":3260280,"end":3260440,"confidence":0.9995117,"speaker":"A"},{"text":"out","start":3260440,"end":3260560,"confidence":0.94921875,"speaker":"A"},{"text":"with","start":3260560,"end":3260680,"confidence":0.9995117,"speaker":"A"},{"text":"the","start":3260680,"end":3260800,"confidence":0.93652344,"speaker":"A"},{"text":"Open","start":3260800,"end":3261000,"confidence":0.9995117,"speaker":"A"},{"text":"API","start":3261000,"end":3261520,"confidence":0.9807129,"speaker":"A"},{"text":"generator","start":3261520,"end":3262160,"confidence":0.9995117,"speaker":"A"},{"text":"which","start":3262640,"end":3263000,"confidence":0.99609375,"speaker":"A"},{"text":"was","start":3263000,"end":3263280,"confidence":0.64746094,"speaker":"A"},{"text":"like,","start":3263280,"end":3263480,"confidence":0.97558594,"speaker":"A"},{"text":"oh","start":3263480,"end":3263760,"confidence":0.91674805,"speaker":"A"},{"text":"wait,","start":3263760,"end":3264160,"confidence":0.9980469,"speaker":"A"},{"text":"what","start":3264160,"end":3264440,"confidence":0.99121094,"speaker":"A"},{"text":"if","start":3264440,"end":3264720,"confidence":0.99853516,"speaker":"A"},{"text":"I","start":3264800,"end":3265040,"confidence":0.9980469,"speaker":"A"},{"text":"can","start":3265040,"end":3265160,"confidence":0.99658203,"speaker":"A"},{"text":"create","start":3265160,"end":3265320,"confidence":0.99902344,"speaker":"A"},{"text":"an","start":3265320,"end":3265480,"confidence":0.96777344,"speaker":"A"},{"text":"open","start":3265480,"end":3265720,"confidence":0.9995117,"speaker":"A"},{"text":"API","start":3265720,"end":3266320,"confidence":0.98046875,"speaker":"A"},{"text":"file","start":3266800,"end":3267280,"confidence":0.98046875,"speaker":"A"},{"text":"out","start":3267520,"end":3267840,"confidence":0.99560547,"speaker":"A"},{"text":"of","start":3267840,"end":3268160,"confidence":0.99853516,"speaker":"A"},{"text":"Apple's","start":3268320,"end":3269040,"confidence":0.9937744,"speaker":"A"},{"text":"10","start":3269280,"end":3269600,"confidence":0.99951,"speaker":"A"},{"text":"year","start":3269600,"end":3269800,"confidence":0.9995117,"speaker":"A"},{"text":"old","start":3269800,"end":3270000,"confidence":0.99902344,"speaker":"A"},{"text":"documentation?","start":3270000,"end":3270800,"confidence":0.9923828,"speaker":"A"}]},{"text":"That'd be a lot of work, but I could do it. And I don't know if you heard, but there was this thing that came out a couple years ago called AI and it's really good at creating documentation for your code, but it's also really good at creating code for your documentation. And so I was like, oh yeah, this is great. Like I can just, I can just Feed it the documentation and go from there. And, like, basically, I've been going step by step through.","start":3273120,"end":3305140,"confidence":0.8873698,"words":[{"text":"That'd","start":3273120,"end":3273520,"confidence":0.8873698,"speaker":"A"},{"text":"be","start":3273520,"end":3273640,"confidence":1,"speaker":"A"},{"text":"a","start":3273640,"end":3273760,"confidence":0.99902344,"speaker":"A"},{"text":"lot","start":3273760,"end":3273840,"confidence":1,"speaker":"A"},{"text":"of","start":3273840,"end":3273960,"confidence":0.9975586,"speaker":"A"},{"text":"work,","start":3273960,"end":3274160,"confidence":1,"speaker":"A"},{"text":"but","start":3274160,"end":3274400,"confidence":0.6777344,"speaker":"A"},{"text":"I","start":3274400,"end":3274600,"confidence":0.9995117,"speaker":"A"},{"text":"could","start":3274600,"end":3274760,"confidence":0.98876953,"speaker":"A"},{"text":"do","start":3274760,"end":3274920,"confidence":0.9995117,"speaker":"A"},{"text":"it.","start":3274920,"end":3275200,"confidence":0.9995117,"speaker":"A"},{"text":"And","start":3275520,"end":3275920,"confidence":0.8173828,"speaker":"A"},{"text":"I","start":3276000,"end":3276280,"confidence":0.99902344,"speaker":"A"},{"text":"don't","start":3276280,"end":3276480,"confidence":0.9949544,"speaker":"A"},{"text":"know","start":3276480,"end":3276560,"confidence":0.99902344,"speaker":"A"},{"text":"if","start":3276560,"end":3276640,"confidence":1,"speaker":"A"},{"text":"you","start":3276640,"end":3276760,"confidence":0.9995117,"speaker":"A"},{"text":"heard,","start":3276760,"end":3277120,"confidence":0.99902344,"speaker":"A"},{"text":"but","start":3277600,"end":3278000,"confidence":0.9921875,"speaker":"A"},{"text":"there","start":3278960,"end":3279240,"confidence":0.9995117,"speaker":"A"},{"text":"was","start":3279240,"end":3279400,"confidence":0.9589844,"speaker":"A"},{"text":"this","start":3279400,"end":3279560,"confidence":0.9746094,"speaker":"A"},{"text":"thing","start":3279560,"end":3279720,"confidence":0.9995117,"speaker":"A"},{"text":"that","start":3279720,"end":3279840,"confidence":0.99902344,"speaker":"A"},{"text":"came","start":3279840,"end":3279960,"confidence":0.99853516,"speaker":"A"},{"text":"out","start":3279960,"end":3280240,"confidence":0.9980469,"speaker":"A"},{"text":"a","start":3280240,"end":3280480,"confidence":0.99853516,"speaker":"A"},{"text":"couple","start":3280480,"end":3280720,"confidence":0.9992676,"speaker":"A"},{"text":"years","start":3280720,"end":3280920,"confidence":0.9995117,"speaker":"A"},{"text":"ago","start":3280920,"end":3281200,"confidence":0.9980469,"speaker":"A"},{"text":"called","start":3281780,"end":3282020,"confidence":0.99609375,"speaker":"A"},{"text":"AI","start":3282580,"end":3283220,"confidence":0.95092773,"speaker":"A"},{"text":"and","start":3283940,"end":3284340,"confidence":0.9873047,"speaker":"A"},{"text":"it's","start":3284980,"end":3285340,"confidence":0.9996745,"speaker":"A"},{"text":"really","start":3285340,"end":3285500,"confidence":0.9995117,"speaker":"A"},{"text":"good","start":3285500,"end":3285700,"confidence":0.9995117,"speaker":"A"},{"text":"at","start":3285700,"end":3285900,"confidence":0.98095703,"speaker":"A"},{"text":"creating","start":3285900,"end":3286260,"confidence":0.9995117,"speaker":"A"},{"text":"documentation","start":3286260,"end":3286940,"confidence":0.99990237,"speaker":"A"},{"text":"for","start":3286940,"end":3287180,"confidence":1,"speaker":"A"},{"text":"your","start":3287180,"end":3287340,"confidence":0.9995117,"speaker":"A"},{"text":"code,","start":3287340,"end":3287660,"confidence":0.94222003,"speaker":"A"},{"text":"but","start":3287660,"end":3287900,"confidence":0.9975586,"speaker":"A"},{"text":"it's","start":3287900,"end":3288100,"confidence":0.9998372,"speaker":"A"},{"text":"also","start":3288100,"end":3288260,"confidence":0.9995117,"speaker":"A"},{"text":"really","start":3288260,"end":3288500,"confidence":0.5620117,"speaker":"A"},{"text":"good","start":3288500,"end":3288700,"confidence":0.9995117,"speaker":"A"},{"text":"at","start":3288700,"end":3288860,"confidence":0.9995117,"speaker":"A"},{"text":"creating","start":3288860,"end":3289140,"confidence":0.96777344,"speaker":"A"},{"text":"code","start":3289140,"end":3289420,"confidence":0.9996745,"speaker":"A"},{"text":"for","start":3289420,"end":3289620,"confidence":0.9995117,"speaker":"A"},{"text":"your","start":3289620,"end":3289820,"confidence":0.9995117,"speaker":"A"},{"text":"documentation.","start":3289820,"end":3290500,"confidence":0.99902344,"speaker":"A"},{"text":"And","start":3291300,"end":3291580,"confidence":0.8925781,"speaker":"A"},{"text":"so","start":3291580,"end":3291700,"confidence":0.99902344,"speaker":"A"},{"text":"I","start":3291700,"end":3291820,"confidence":0.9975586,"speaker":"A"},{"text":"was","start":3291820,"end":3292020,"confidence":0.9995117,"speaker":"A"},{"text":"like,","start":3292020,"end":3292340,"confidence":0.99658203,"speaker":"A"},{"text":"oh","start":3292500,"end":3292980,"confidence":0.9580078,"speaker":"A"},{"text":"yeah,","start":3293460,"end":3293940,"confidence":0.9975586,"speaker":"A"},{"text":"this","start":3293940,"end":3294220,"confidence":0.9951172,"speaker":"A"},{"text":"is","start":3294220,"end":3294380,"confidence":0.99853516,"speaker":"A"},{"text":"great.","start":3294380,"end":3294660,"confidence":0.9980469,"speaker":"A"},{"text":"Like","start":3295060,"end":3295460,"confidence":0.9238281,"speaker":"A"},{"text":"I","start":3295460,"end":3295740,"confidence":0.9707031,"speaker":"A"},{"text":"can","start":3295740,"end":3295900,"confidence":0.99658203,"speaker":"A"},{"text":"just,","start":3295900,"end":3296180,"confidence":0.9980469,"speaker":"A"},{"text":"I","start":3296740,"end":3296980,"confidence":0.97753906,"speaker":"A"},{"text":"can","start":3296980,"end":3297140,"confidence":0.7270508,"speaker":"A"},{"text":"just","start":3297140,"end":3297420,"confidence":0.9995117,"speaker":"A"},{"text":"Feed","start":3297420,"end":3297739,"confidence":0.9968262,"speaker":"A"},{"text":"it","start":3297739,"end":3297900,"confidence":0.8671875,"speaker":"A"},{"text":"the","start":3297900,"end":3298060,"confidence":0.99853516,"speaker":"A"},{"text":"documentation","start":3298060,"end":3298740,"confidence":0.99921876,"speaker":"A"},{"text":"and","start":3298980,"end":3299380,"confidence":0.9238281,"speaker":"A"},{"text":"go","start":3301140,"end":3301420,"confidence":0.9970703,"speaker":"A"},{"text":"from","start":3301420,"end":3301620,"confidence":0.9995117,"speaker":"A"},{"text":"there.","start":3301620,"end":3301940,"confidence":0.9995117,"speaker":"A"},{"text":"And,","start":3302020,"end":3302340,"confidence":0.97998047,"speaker":"A"},{"text":"like,","start":3302340,"end":3302660,"confidence":0.9477539,"speaker":"A"},{"text":"basically,","start":3302820,"end":3303300,"confidence":0.99975586,"speaker":"A"},{"text":"I've","start":3303300,"end":3303540,"confidence":0.99072266,"speaker":"A"},{"text":"been","start":3303540,"end":3303660,"confidence":0.9902344,"speaker":"A"},{"text":"going","start":3303660,"end":3303860,"confidence":0.9995117,"speaker":"A"},{"text":"step","start":3303860,"end":3304060,"confidence":0.9995117,"speaker":"A"},{"text":"by","start":3304060,"end":3304260,"confidence":1,"speaker":"A"},{"text":"step","start":3304260,"end":3304580,"confidence":1,"speaker":"A"},{"text":"through.","start":3304740,"end":3305140,"confidence":0.98876953,"speaker":"A"}]},{"text":"Like I said, if you looked at the miskit repo, like, I'm going through step by step and adding new APIs based on what's available in the documentation, piece by piece. And I would say at this point, it's like most of the really, like 80% of that people use is there. There's like, stuff like subscriptions and zones that I'm still trying to figure out, but it's. It's pretty close to done at this point. Mm.","start":3305940,"end":3331900,"confidence":0.9980469,"words":[{"text":"Like","start":3305940,"end":3306260,"confidence":0.9980469,"speaker":"A"},{"text":"I","start":3306260,"end":3306460,"confidence":1,"speaker":"A"},{"text":"said,","start":3306460,"end":3306620,"confidence":1,"speaker":"A"},{"text":"if","start":3306620,"end":3306820,"confidence":0.6225586,"speaker":"A"},{"text":"you","start":3306820,"end":3306980,"confidence":1,"speaker":"A"},{"text":"looked","start":3306980,"end":3307220,"confidence":0.9802246,"speaker":"A"},{"text":"at","start":3307220,"end":3307340,"confidence":0.9995117,"speaker":"A"},{"text":"the","start":3307340,"end":3307620,"confidence":0.94140625,"speaker":"A"},{"text":"miskit","start":3307700,"end":3308500,"confidence":0.876709,"speaker":"A"},{"text":"repo,","start":3308780,"end":3309300,"confidence":0.99072266,"speaker":"A"},{"text":"like,","start":3309300,"end":3309580,"confidence":0.9838867,"speaker":"A"},{"text":"I'm","start":3309580,"end":3309820,"confidence":0.9995117,"speaker":"A"},{"text":"going","start":3309820,"end":3309940,"confidence":0.9995117,"speaker":"A"},{"text":"through","start":3309940,"end":3310140,"confidence":0.9995117,"speaker":"A"},{"text":"step","start":3310140,"end":3310340,"confidence":0.9946289,"speaker":"A"},{"text":"by","start":3310340,"end":3310500,"confidence":0.99902344,"speaker":"A"},{"text":"step","start":3310500,"end":3310660,"confidence":1,"speaker":"A"},{"text":"and","start":3310660,"end":3310820,"confidence":0.93896484,"speaker":"A"},{"text":"adding","start":3310820,"end":3311260,"confidence":0.998291,"speaker":"A"},{"text":"new","start":3311660,"end":3312060,"confidence":0.9995117,"speaker":"A"},{"text":"APIs","start":3312380,"end":3313100,"confidence":0.98168945,"speaker":"A"},{"text":"based","start":3314300,"end":3314620,"confidence":0.9995117,"speaker":"A"},{"text":"on","start":3314620,"end":3314780,"confidence":0.9995117,"speaker":"A"},{"text":"what's","start":3314780,"end":3315020,"confidence":0.9996745,"speaker":"A"},{"text":"available","start":3315020,"end":3315220,"confidence":1,"speaker":"A"},{"text":"in","start":3315220,"end":3315460,"confidence":0.95654297,"speaker":"A"},{"text":"the","start":3315460,"end":3315580,"confidence":0.99902344,"speaker":"A"},{"text":"documentation,","start":3315580,"end":3316300,"confidence":0.99677736,"speaker":"A"},{"text":"piece","start":3316700,"end":3317060,"confidence":0.9938151,"speaker":"A"},{"text":"by","start":3317060,"end":3317220,"confidence":0.9291992,"speaker":"A"},{"text":"piece.","start":3317220,"end":3317500,"confidence":0.99332684,"speaker":"A"},{"text":"And","start":3317500,"end":3317660,"confidence":0.99121094,"speaker":"A"},{"text":"I","start":3317660,"end":3317740,"confidence":0.9995117,"speaker":"A"},{"text":"would","start":3317740,"end":3317820,"confidence":1,"speaker":"A"},{"text":"say","start":3317820,"end":3317940,"confidence":1,"speaker":"A"},{"text":"at","start":3317940,"end":3318060,"confidence":0.9995117,"speaker":"A"},{"text":"this","start":3318060,"end":3318180,"confidence":1,"speaker":"A"},{"text":"point,","start":3318180,"end":3318340,"confidence":0.99902344,"speaker":"A"},{"text":"it's","start":3318340,"end":3318580,"confidence":0.9899089,"speaker":"A"},{"text":"like","start":3318580,"end":3318860,"confidence":0.9975586,"speaker":"A"},{"text":"most","start":3319340,"end":3319660,"confidence":1,"speaker":"A"},{"text":"of","start":3319660,"end":3319820,"confidence":0.99902344,"speaker":"A"},{"text":"the","start":3319820,"end":3320020,"confidence":0.99658203,"speaker":"A"},{"text":"really,","start":3320020,"end":3320380,"confidence":0.99658203,"speaker":"A"},{"text":"like","start":3320620,"end":3320940,"confidence":0.98876953,"speaker":"A"},{"text":"80%","start":3320940,"end":3321500,"confidence":0.96655,"speaker":"A"},{"text":"of","start":3321500,"end":3321780,"confidence":0.7285156,"speaker":"A"},{"text":"that","start":3321780,"end":3321940,"confidence":0.9941406,"speaker":"A"},{"text":"people","start":3321940,"end":3322140,"confidence":1,"speaker":"A"},{"text":"use","start":3322140,"end":3322420,"confidence":0.9995117,"speaker":"A"},{"text":"is","start":3322420,"end":3322660,"confidence":0.98876953,"speaker":"A"},{"text":"there.","start":3322660,"end":3322940,"confidence":0.9951172,"speaker":"A"},{"text":"There's","start":3322940,"end":3323340,"confidence":0.9998372,"speaker":"A"},{"text":"like,","start":3323340,"end":3323500,"confidence":0.99121094,"speaker":"A"},{"text":"stuff","start":3323500,"end":3323780,"confidence":0.9995117,"speaker":"A"},{"text":"like","start":3323780,"end":3323980,"confidence":0.99902344,"speaker":"A"},{"text":"subscriptions","start":3323980,"end":3324619,"confidence":0.99501956,"speaker":"A"},{"text":"and","start":3324619,"end":3324940,"confidence":0.99658203,"speaker":"A"},{"text":"zones","start":3324940,"end":3325300,"confidence":0.95703125,"speaker":"A"},{"text":"that","start":3325300,"end":3325660,"confidence":0.99316406,"speaker":"A"},{"text":"I'm","start":3325980,"end":3326340,"confidence":0.9868164,"speaker":"A"},{"text":"still","start":3326340,"end":3326500,"confidence":0.9975586,"speaker":"A"},{"text":"trying","start":3326500,"end":3326700,"confidence":0.9995117,"speaker":"A"},{"text":"to","start":3326700,"end":3326860,"confidence":0.9995117,"speaker":"A"},{"text":"figure","start":3326860,"end":3327140,"confidence":0.99975586,"speaker":"A"},{"text":"out,","start":3327140,"end":3327420,"confidence":0.99121094,"speaker":"A"},{"text":"but","start":3328460,"end":3328780,"confidence":0.9941406,"speaker":"A"},{"text":"it's.","start":3328780,"end":3329100,"confidence":0.9900716,"speaker":"A"},{"text":"It's","start":3329100,"end":3329340,"confidence":0.98746747,"speaker":"A"},{"text":"pretty","start":3329340,"end":3329540,"confidence":0.9991862,"speaker":"A"},{"text":"close","start":3329540,"end":3329740,"confidence":0.9995117,"speaker":"A"},{"text":"to","start":3329740,"end":3329980,"confidence":0.9975586,"speaker":"A"},{"text":"done","start":3329980,"end":3330260,"confidence":0.95410156,"speaker":"A"},{"text":"at","start":3330260,"end":3330460,"confidence":0.99902344,"speaker":"A"},{"text":"this","start":3330460,"end":3330620,"confidence":0.95751953,"speaker":"A"},{"text":"point.","start":3330620,"end":3330940,"confidence":0.66552734,"speaker":"A"},{"text":"Mm.","start":3331260,"end":3331900,"confidence":0.62402344,"speaker":"B"}]},{"text":"If you use it. Yeah, it's one of those. Because I. Go ahead. Yeah.","start":3335110,"end":3338950,"confidence":0.56103516,"words":[{"text":"If","start":3335110,"end":3335230,"confidence":0.56103516,"speaker":"A"},{"text":"you","start":3335230,"end":3335350,"confidence":0.99902344,"speaker":"A"},{"text":"use","start":3335350,"end":3335510,"confidence":0.9975586,"speaker":"A"},{"text":"it.","start":3335510,"end":3335830,"confidence":0.5029297,"speaker":"A"},{"text":"Yeah,","start":3336230,"end":3336550,"confidence":0.9943034,"speaker":"C"},{"text":"it's","start":3336550,"end":3336630,"confidence":0.94905597,"speaker":"C"},{"text":"one","start":3336630,"end":3336750,"confidence":0.9902344,"speaker":"C"},{"text":"of","start":3336750,"end":3336870,"confidence":0.99853516,"speaker":"C"},{"text":"those.","start":3336870,"end":3337110,"confidence":0.9760742,"speaker":"C"},{"text":"Because","start":3337270,"end":3337630,"confidence":0.7348633,"speaker":"A"},{"text":"I.","start":3337630,"end":3337990,"confidence":0.86621094,"speaker":"A"},{"text":"Go","start":3338070,"end":3338350,"confidence":0.9902344,"speaker":"A"},{"text":"ahead.","start":3338350,"end":3338590,"confidence":0.9980469,"speaker":"A"},{"text":"Yeah.","start":3338590,"end":3338950,"confidence":0.99397784,"speaker":"C"}]},{"text":"I was gonna say it's one of those projects that makes me want to set up a. Like a vapor server or something just to do some Swift on the server. Yeah. Or just like, I wonder if there's like, something you do on a pie, like just hook it up to a CloudKit database. Like, there's a lot you could do here because all you need is decent os.","start":3338950,"end":3357510,"confidence":0.49267578,"words":[{"text":"I","start":3338950,"end":3339110,"confidence":0.49267578,"speaker":"C"},{"text":"was","start":3339110,"end":3339230,"confidence":0.9189453,"speaker":"C"},{"text":"gonna","start":3339230,"end":3339430,"confidence":0.83776855,"speaker":"C"},{"text":"say","start":3339430,"end":3339510,"confidence":1,"speaker":"C"},{"text":"it's","start":3339510,"end":3339670,"confidence":0.9998372,"speaker":"C"},{"text":"one","start":3339670,"end":3339750,"confidence":1,"speaker":"C"},{"text":"of","start":3339750,"end":3339830,"confidence":0.9995117,"speaker":"C"},{"text":"those","start":3339830,"end":3339950,"confidence":0.9995117,"speaker":"C"},{"text":"projects","start":3339950,"end":3340310,"confidence":0.99975586,"speaker":"C"},{"text":"that","start":3340310,"end":3340430,"confidence":1,"speaker":"C"},{"text":"makes","start":3340430,"end":3340590,"confidence":0.9995117,"speaker":"C"},{"text":"me","start":3340590,"end":3340750,"confidence":0.9995117,"speaker":"C"},{"text":"want","start":3340750,"end":3340910,"confidence":0.9604492,"speaker":"C"},{"text":"to","start":3340910,"end":3341070,"confidence":1,"speaker":"C"},{"text":"set","start":3341070,"end":3341230,"confidence":1,"speaker":"C"},{"text":"up","start":3341230,"end":3341390,"confidence":0.9995117,"speaker":"C"},{"text":"a.","start":3341390,"end":3341670,"confidence":0.96240234,"speaker":"C"},{"text":"Like","start":3342150,"end":3342470,"confidence":0.9941406,"speaker":"C"},{"text":"a","start":3342470,"end":3342750,"confidence":0.99902344,"speaker":"C"},{"text":"vapor","start":3342750,"end":3343310,"confidence":0.98551434,"speaker":"C"},{"text":"server","start":3343310,"end":3343630,"confidence":0.9995117,"speaker":"C"},{"text":"or","start":3343630,"end":3343790,"confidence":0.99853516,"speaker":"C"},{"text":"something","start":3343790,"end":3344030,"confidence":1,"speaker":"C"},{"text":"just","start":3344030,"end":3344270,"confidence":1,"speaker":"C"},{"text":"to","start":3344270,"end":3344390,"confidence":1,"speaker":"C"},{"text":"do","start":3344390,"end":3344510,"confidence":0.9995117,"speaker":"C"},{"text":"some","start":3344510,"end":3344670,"confidence":1,"speaker":"C"},{"text":"Swift","start":3344670,"end":3344990,"confidence":0.99975586,"speaker":"C"},{"text":"on","start":3344990,"end":3345110,"confidence":1,"speaker":"C"},{"text":"the","start":3345110,"end":3345230,"confidence":1,"speaker":"C"},{"text":"server.","start":3345230,"end":3345670,"confidence":0.99975586,"speaker":"C"},{"text":"Yeah.","start":3346630,"end":3347110,"confidence":0.9916992,"speaker":"A"},{"text":"Or","start":3347270,"end":3347590,"confidence":0.92041016,"speaker":"A"},{"text":"just","start":3347590,"end":3347830,"confidence":0.99902344,"speaker":"A"},{"text":"like,","start":3347830,"end":3348150,"confidence":0.99658203,"speaker":"A"},{"text":"I","start":3348870,"end":3349150,"confidence":0.9760742,"speaker":"A"},{"text":"wonder","start":3349150,"end":3349390,"confidence":0.9980469,"speaker":"A"},{"text":"if","start":3349390,"end":3349510,"confidence":0.6303711,"speaker":"A"},{"text":"there's","start":3349510,"end":3349710,"confidence":0.867513,"speaker":"A"},{"text":"like,","start":3349710,"end":3349830,"confidence":0.9819336,"speaker":"A"},{"text":"something","start":3349830,"end":3349990,"confidence":0.99902344,"speaker":"A"},{"text":"you","start":3349990,"end":3350189,"confidence":0.9926758,"speaker":"A"},{"text":"do","start":3350189,"end":3350309,"confidence":0.99853516,"speaker":"A"},{"text":"on","start":3350309,"end":3350430,"confidence":0.9970703,"speaker":"A"},{"text":"a","start":3350430,"end":3350590,"confidence":0.9946289,"speaker":"A"},{"text":"pie,","start":3350590,"end":3350950,"confidence":0.7319336,"speaker":"A"},{"text":"like","start":3351750,"end":3352150,"confidence":0.97265625,"speaker":"A"},{"text":"just","start":3352230,"end":3352470,"confidence":0.99853516,"speaker":"A"},{"text":"hook","start":3352470,"end":3352630,"confidence":0.99902344,"speaker":"A"},{"text":"it","start":3352630,"end":3352750,"confidence":0.99853516,"speaker":"A"},{"text":"up","start":3352750,"end":3352870,"confidence":1,"speaker":"A"},{"text":"to","start":3352870,"end":3352990,"confidence":1,"speaker":"A"},{"text":"a","start":3352990,"end":3353110,"confidence":0.9946289,"speaker":"A"},{"text":"CloudKit","start":3353110,"end":3353550,"confidence":0.9953613,"speaker":"A"},{"text":"database.","start":3353550,"end":3353990,"confidence":1,"speaker":"A"},{"text":"Like,","start":3353990,"end":3354190,"confidence":0.99121094,"speaker":"A"},{"text":"there's","start":3354190,"end":3354430,"confidence":0.9998372,"speaker":"A"},{"text":"a","start":3354430,"end":3354550,"confidence":1,"speaker":"A"},{"text":"lot","start":3354550,"end":3354710,"confidence":1,"speaker":"A"},{"text":"you","start":3354710,"end":3354870,"confidence":1,"speaker":"A"},{"text":"could","start":3354870,"end":3354990,"confidence":0.98828125,"speaker":"A"},{"text":"do","start":3354990,"end":3355150,"confidence":1,"speaker":"A"},{"text":"here","start":3355150,"end":3355350,"confidence":1,"speaker":"A"},{"text":"because","start":3355350,"end":3355550,"confidence":0.8598633,"speaker":"A"},{"text":"all","start":3355550,"end":3355710,"confidence":0.9995117,"speaker":"A"},{"text":"you","start":3355710,"end":3355870,"confidence":1,"speaker":"A"},{"text":"need","start":3355870,"end":3356030,"confidence":0.99902344,"speaker":"A"},{"text":"is","start":3356030,"end":3356310,"confidence":0.97314453,"speaker":"A"},{"text":"decent","start":3356710,"end":3357150,"confidence":0.9091797,"speaker":"A"},{"text":"os.","start":3357150,"end":3357510,"confidence":0.95581055,"speaker":"A"}]},{"text":"I don't know anything about sharing. I haven't done anything with sharing yet, so I still have to do that and a few other things, but. No, yeah,. It's an interesting idea. Thank you.","start":3358950,"end":3370460,"confidence":0.9995117,"words":[{"text":"I","start":3358950,"end":3359230,"confidence":0.9995117,"speaker":"A"},{"text":"don't","start":3359230,"end":3359430,"confidence":0.9998372,"speaker":"A"},{"text":"know","start":3359430,"end":3359550,"confidence":0.9995117,"speaker":"A"},{"text":"anything","start":3359550,"end":3359870,"confidence":0.99975586,"speaker":"A"},{"text":"about","start":3359870,"end":3360030,"confidence":0.9995117,"speaker":"A"},{"text":"sharing.","start":3360030,"end":3360430,"confidence":0.9663086,"speaker":"A"},{"text":"I","start":3360430,"end":3360670,"confidence":1,"speaker":"A"},{"text":"haven't","start":3360670,"end":3360870,"confidence":0.9992676,"speaker":"A"},{"text":"done","start":3360870,"end":3360990,"confidence":0.9995117,"speaker":"A"},{"text":"anything","start":3360990,"end":3361310,"confidence":0.99975586,"speaker":"A"},{"text":"with","start":3361310,"end":3361470,"confidence":0.8676758,"speaker":"A"},{"text":"sharing","start":3361470,"end":3361830,"confidence":0.99731445,"speaker":"A"},{"text":"yet,","start":3361830,"end":3362110,"confidence":0.98779297,"speaker":"A"},{"text":"so","start":3362110,"end":3362310,"confidence":0.99902344,"speaker":"A"},{"text":"I","start":3362310,"end":3362430,"confidence":0.9663086,"speaker":"A"},{"text":"still","start":3362430,"end":3362590,"confidence":0.9589844,"speaker":"A"},{"text":"have","start":3362590,"end":3362750,"confidence":0.77441406,"speaker":"A"},{"text":"to","start":3362750,"end":3362870,"confidence":0.9995117,"speaker":"A"},{"text":"do","start":3362870,"end":3362990,"confidence":0.9951172,"speaker":"A"},{"text":"that","start":3362990,"end":3363190,"confidence":1,"speaker":"A"},{"text":"and","start":3363190,"end":3363390,"confidence":0.99853516,"speaker":"A"},{"text":"a","start":3363390,"end":3363510,"confidence":0.9995117,"speaker":"A"},{"text":"few","start":3363510,"end":3363630,"confidence":1,"speaker":"A"},{"text":"other","start":3363630,"end":3363830,"confidence":0.99902344,"speaker":"A"},{"text":"things,","start":3363830,"end":3364070,"confidence":0.9995117,"speaker":"A"},{"text":"but.","start":3364070,"end":3364390,"confidence":0.98876953,"speaker":"A"},{"text":"No,","start":3364940,"end":3365180,"confidence":0.6020508,"speaker":"A"},{"text":"yeah,.","start":3365180,"end":3365740,"confidence":0.9869792,"speaker":"A"},{"text":"It's","start":3367740,"end":3368060,"confidence":0.97021484,"speaker":"C"},{"text":"an","start":3368060,"end":3368180,"confidence":0.99609375,"speaker":"C"},{"text":"interesting","start":3368180,"end":3368500,"confidence":0.99975586,"speaker":"C"},{"text":"idea.","start":3368500,"end":3368940,"confidence":0.98706055,"speaker":"C"},{"text":"Thank","start":3369900,"end":3370220,"confidence":0.9868164,"speaker":"A"},{"text":"you.","start":3370220,"end":3370460,"confidence":0.9975586,"speaker":"A"}]},{"text":"Yeah. Well, thank you for joining, Josh. Yeah. Thanks for hosting this and sharing this info. It's nice.","start":3371420,"end":3377340,"confidence":0.88997394,"words":[{"text":"Yeah.","start":3371420,"end":3371900,"confidence":0.88997394,"speaker":"B"},{"text":"Well,","start":3371900,"end":3372100,"confidence":0.9980469,"speaker":"A"},{"text":"thank","start":3372100,"end":3372300,"confidence":1,"speaker":"A"},{"text":"you","start":3372300,"end":3372420,"confidence":0.9995117,"speaker":"A"},{"text":"for","start":3372420,"end":3372580,"confidence":0.99902344,"speaker":"A"},{"text":"joining,","start":3372580,"end":3372860,"confidence":0.96809894,"speaker":"A"},{"text":"Josh.","start":3372860,"end":3373260,"confidence":0.98461914,"speaker":"A"},{"text":"Yeah.","start":3373660,"end":3374060,"confidence":0.81844074,"speaker":"C"},{"text":"Thanks","start":3374060,"end":3374300,"confidence":1,"speaker":"C"},{"text":"for","start":3374300,"end":3374460,"confidence":0.9995117,"speaker":"C"},{"text":"hosting","start":3374460,"end":3374820,"confidence":0.9995117,"speaker":"C"},{"text":"this","start":3374820,"end":3375020,"confidence":0.9707031,"speaker":"C"},{"text":"and","start":3375020,"end":3375340,"confidence":0.99902344,"speaker":"C"},{"text":"sharing","start":3375900,"end":3376340,"confidence":0.9934082,"speaker":"C"},{"text":"this","start":3376340,"end":3376500,"confidence":0.9995117,"speaker":"C"},{"text":"info.","start":3376500,"end":3376820,"confidence":0.9995117,"speaker":"C"},{"text":"It's","start":3376820,"end":3377020,"confidence":0.9941406,"speaker":"C"},{"text":"nice.","start":3377020,"end":3377340,"confidence":1,"speaker":"C"}]},{"text":"Yeah. If you ever run into anything, let me know. Will do. All right, talk to you later. All right, sounds good.","start":3378060,"end":3385180,"confidence":0.9866536,"words":[{"text":"Yeah.","start":3378060,"end":3378540,"confidence":0.9866536,"speaker":"A"},{"text":"If","start":3378620,"end":3378980,"confidence":0.9794922,"speaker":"A"},{"text":"you","start":3378980,"end":3379260,"confidence":0.9995117,"speaker":"A"},{"text":"ever","start":3379260,"end":3379500,"confidence":1,"speaker":"A"},{"text":"run","start":3379500,"end":3379700,"confidence":0.9995117,"speaker":"A"},{"text":"into","start":3379700,"end":3379860,"confidence":1,"speaker":"A"},{"text":"anything,","start":3379860,"end":3380180,"confidence":1,"speaker":"A"},{"text":"let","start":3380180,"end":3380300,"confidence":1,"speaker":"A"},{"text":"me","start":3380300,"end":3380459,"confidence":1,"speaker":"A"},{"text":"know.","start":3380459,"end":3380780,"confidence":0.9995117,"speaker":"A"},{"text":"Will","start":3381420,"end":3381740,"confidence":0.5800781,"speaker":"A"},{"text":"do.","start":3381740,"end":3382060,"confidence":0.99365234,"speaker":"A"},{"text":"All","start":3382940,"end":3383220,"confidence":0.9814453,"speaker":"A"},{"text":"right,","start":3383220,"end":3383500,"confidence":1,"speaker":"A"},{"text":"talk","start":3383660,"end":3383940,"confidence":1,"speaker":"A"},{"text":"to","start":3383940,"end":3384100,"confidence":1,"speaker":"A"},{"text":"you","start":3384100,"end":3384220,"confidence":0.9995117,"speaker":"A"},{"text":"later.","start":3384220,"end":3384420,"confidence":1,"speaker":"A"},{"text":"All","start":3384420,"end":3384620,"confidence":0.9223633,"speaker":"A"},{"text":"right,","start":3384620,"end":3384780,"confidence":0.9145508,"speaker":"A"},{"text":"sounds","start":3384780,"end":3385020,"confidence":1,"speaker":"A"},{"text":"good.","start":3385020,"end":3385180,"confidence":1,"speaker":"A"}]},{"text":"See you. Bye. Bye.","start":3385180,"end":3387340,"confidence":0.9975586,"words":[{"text":"See","start":3385180,"end":3385380,"confidence":0.9975586,"speaker":"C"},{"text":"you.","start":3385380,"end":3385660,"confidence":0.54296875,"speaker":"C"},{"text":"Bye.","start":3386220,"end":3386700,"confidence":0.9375,"speaker":"A"},{"text":"Bye.","start":3386860,"end":3387340,"confidence":0.9519043,"speaker":"C"}]}],"id":"8a542ac0-f58a-4b02-b801-9926da98bdd0","confidence":0.97097707,"audio_duration":3388} \ No newline at end of file diff --git a/docs/transcriptions/timestamps.json b/docs/transcriptions/timestamps.json new file mode 100644 index 00000000..f31f7d33 --- /dev/null +++ b/docs/transcriptions/timestamps.json @@ -0,0 +1 @@ +[{"text":"Hey,","start":262980,"end":263180,"confidence":0.99658203,"speaker":"A"},{"text":"Evan,","start":263180,"end":263580,"confidence":0.99609375,"speaker":"A"},{"text":"can","start":263580,"end":263700,"confidence":0.9995117,"speaker":"A"},{"text":"you","start":263700,"end":263780,"confidence":0.99316406,"speaker":"A"},{"text":"hear","start":263780,"end":263900,"confidence":1,"speaker":"A"},{"text":"me","start":263900,"end":264020,"confidence":1,"speaker":"A"},{"text":"all","start":264020,"end":264140,"confidence":0.87158203,"speaker":"A"},{"text":"right?","start":264140,"end":264420,"confidence":0.96240234,"speaker":"A"},{"text":"Yeah,","start":264660,"end":265020,"confidence":0.9741211,"speaker":"B"},{"text":"I","start":265020,"end":265140,"confidence":1,"speaker":"B"},{"text":"can","start":265140,"end":265260,"confidence":1,"speaker":"B"},{"text":"hear","start":265260,"end":265420,"confidence":1,"speaker":"B"},{"text":"you.","start":265420,"end":265700,"confidence":0.99365234,"speaker":"B"},{"text":"Awesome.","start":266420,"end":267060,"confidence":0.9998372,"speaker":"A"},{"text":"How","start":267060,"end":267340,"confidence":1,"speaker":"A"},{"text":"do","start":267340,"end":267500,"confidence":1,"speaker":"A"},{"text":"I","start":267500,"end":267660,"confidence":1,"speaker":"A"},{"text":"sound?","start":267660,"end":268020,"confidence":0.99975586,"speaker":"A"},{"text":"Good.","start":268340,"end":268740,"confidence":0.99902344,"speaker":"A"},{"text":"I've","start":270260,"end":270740,"confidence":0.7714844,"speaker":"A"},{"text":"used","start":270740,"end":270940,"confidence":0.99316406,"speaker":"A"},{"text":"this","start":270940,"end":271140,"confidence":0.9736328,"speaker":"A"},{"text":"microphone","start":271140,"end":271660,"confidence":0.9484375,"speaker":"A"},{"text":"in","start":271660,"end":271820,"confidence":0.9946289,"speaker":"A"},{"text":"ages.","start":271820,"end":272340,"confidence":0.9995117,"speaker":"A"},{"text":"It's","start":273060,"end":273420,"confidence":0.99397784,"speaker":"A"},{"text":"like","start":273420,"end":273580,"confidence":0.99121094,"speaker":"A"},{"text":"all","start":273580,"end":273780,"confidence":0.98583984,"speaker":"A"},{"text":"dusty.","start":273780,"end":274420,"confidence":0.99934894,"speaker":"A"},{"text":"How","start":281140,"end":281500,"confidence":0.6699219,"speaker":"A"},{"text":"you","start":281500,"end":281700,"confidence":0.97021484,"speaker":"A"},{"text":"think","start":281700,"end":281820,"confidence":1,"speaker":"A"},{"text":"I","start":281820,"end":281940,"confidence":0.99853516,"speaker":"A"},{"text":"should","start":281940,"end":282060,"confidence":0.9995117,"speaker":"A"},{"text":"wait","start":282060,"end":282260,"confidence":0.99975586,"speaker":"A"},{"text":"like","start":282260,"end":282380,"confidence":0.99316406,"speaker":"A"},{"text":"five","start":282380,"end":282540,"confidence":0.9995117,"speaker":"A"},{"text":"minutes","start":282540,"end":282820,"confidence":1,"speaker":"A"},{"text":"for","start":282820,"end":283020,"confidence":0.9995117,"speaker":"A"},{"text":"people","start":283020,"end":283220,"confidence":0.9995117,"speaker":"A"},{"text":"to","start":283220,"end":283380,"confidence":0.9916992,"speaker":"A"},{"text":"come","start":283380,"end":283540,"confidence":0.9995117,"speaker":"A"},{"text":"in","start":283540,"end":283780,"confidence":0.99902344,"speaker":"A"},{"text":"or.","start":283780,"end":284100,"confidence":0.9394531,"speaker":"A"},{"text":"Probably.","start":284260,"end":284740,"confidence":0.8670247,"speaker":"B"},{"text":"Yeah,","start":284980,"end":285460,"confidence":0.99316406,"speaker":"B"},{"text":"that","start":285770,"end":285970,"confidence":0.72314453,"speaker":"B"},{"text":"there's","start":285970,"end":286410,"confidence":0.8248698,"speaker":"B"},{"text":"if.","start":286490,"end":286890,"confidence":0.97558594,"speaker":"B"},{"text":"Yeah,","start":286970,"end":287530,"confidence":0.99869794,"speaker":"B"},{"text":"otherwise","start":288010,"end":288450,"confidence":0.98502606,"speaker":"B"},{"text":"you","start":288450,"end":288570,"confidence":0.99902344,"speaker":"B"},{"text":"can","start":288570,"end":288690,"confidence":0.99902344,"speaker":"B"},{"text":"just.","start":288690,"end":288890,"confidence":1,"speaker":"B"},{"text":"You","start":288890,"end":289090,"confidence":0.99609375,"speaker":"B"},{"text":"could","start":289090,"end":289290,"confidence":0.9824219,"speaker":"B"},{"text":"start,","start":289290,"end":289610,"confidence":0.9995117,"speaker":"B"},{"text":"but","start":289850,"end":290250,"confidence":0.99902344,"speaker":"B"},{"text":"that'll","start":291130,"end":291530,"confidence":0.96761066,"speaker":"B"},{"text":"be","start":291530,"end":291610,"confidence":0.9995117,"speaker":"B"},{"text":"interesting.","start":291610,"end":291930,"confidence":0.99609375,"speaker":"B"},{"text":"Do","start":291930,"end":292090,"confidence":0.7919922,"speaker":"A"},{"text":"you","start":292090,"end":292170,"confidence":0.99560547,"speaker":"A"},{"text":"mind","start":292170,"end":292290,"confidence":0.9995117,"speaker":"A"},{"text":"if","start":292290,"end":292450,"confidence":0.99560547,"speaker":"A"},{"text":"I","start":292450,"end":292650,"confidence":0.9995117,"speaker":"A"},{"text":"grab","start":292650,"end":292930,"confidence":1,"speaker":"A"},{"text":"a","start":292930,"end":293050,"confidence":0.9995117,"speaker":"A"},{"text":"cup","start":293050,"end":293170,"confidence":0.9995117,"speaker":"A"},{"text":"of","start":293170,"end":293330,"confidence":0.9970703,"speaker":"A"},{"text":"coffee","start":293330,"end":293650,"confidence":0.9998372,"speaker":"A"},{"text":"real","start":293650,"end":293810,"confidence":0.9995117,"speaker":"A"},{"text":"quick?","start":293810,"end":294010,"confidence":1,"speaker":"A"},{"text":"No,","start":294010,"end":294250,"confidence":0.9975586,"speaker":"B"},{"text":"not","start":294250,"end":294450,"confidence":1,"speaker":"B"},{"text":"at","start":294450,"end":294570,"confidence":0.9995117,"speaker":"B"},{"text":"all.","start":294570,"end":294730,"confidence":1,"speaker":"B"},{"text":"Not","start":294730,"end":294930,"confidence":0.71875,"speaker":"A"},{"text":"at","start":294930,"end":295010,"confidence":0.8486328,"speaker":"A"},{"text":"all.","start":295010,"end":295210,"confidence":0.9042969,"speaker":"A"},{"text":"Okay,","start":295530,"end":296090,"confidence":0.9946289,"speaker":"A"},{"text":"cool.","start":296730,"end":297210,"confidence":0.99609375,"speaker":"A"},{"text":"I'm","start":297210,"end":297570,"confidence":0.8929036,"speaker":"A"},{"text":"not","start":297570,"end":297730,"confidence":1,"speaker":"A"},{"text":"using","start":297730,"end":297930,"confidence":0.9995117,"speaker":"A"},{"text":"the","start":297930,"end":298090,"confidence":0.99609375,"speaker":"A"},{"text":"AirPods","start":298090,"end":298610,"confidence":0.96594,"speaker":"A"},{"text":"mic,","start":298610,"end":298930,"confidence":0.9863281,"speaker":"A"},{"text":"so","start":298930,"end":299250,"confidence":0.99902344,"speaker":"A"},{"text":"I","start":299250,"end":299490,"confidence":1,"speaker":"A"},{"text":"can","start":299490,"end":299650,"confidence":0.9995117,"speaker":"A"},{"text":"hear","start":299650,"end":299810,"confidence":1,"speaker":"A"},{"text":"you,","start":299810,"end":299970,"confidence":0.9995117,"speaker":"A"},{"text":"but","start":299970,"end":300130,"confidence":1,"speaker":"A"},{"text":"you","start":300130,"end":300290,"confidence":1,"speaker":"A"},{"text":"won't","start":300290,"end":300490,"confidence":0.9998372,"speaker":"A"},{"text":"be","start":300490,"end":300570,"confidence":1,"speaker":"A"},{"text":"able","start":300570,"end":300690,"confidence":1,"speaker":"A"},{"text":"to","start":300690,"end":300850,"confidence":1,"speaker":"A"},{"text":"hear","start":300850,"end":301050,"confidence":0.9995117,"speaker":"A"},{"text":"me.","start":301050,"end":301370,"confidence":0.9995117,"speaker":"A"},{"text":"Okay.","start":301690,"end":302250,"confidence":0.98746747,"speaker":"B"},{"text":"It's.","start":362440,"end":387820,"confidence":0.7732747,"speaker":"A"},{"text":"Thank","start":531699,"end":531940,"confidence":0.9851074,"speaker":"A"},{"text":"you","start":531940,"end":532260,"confidence":1,"speaker":"A"},{"text":"for","start":533860,"end":534220,"confidence":0.59277344,"speaker":"A"},{"text":"your","start":534220,"end":534500,"confidence":1,"speaker":"A"},{"text":"patience.","start":534500,"end":535060,"confidence":0.9992676,"speaker":"A"},{"text":"So","start":549010,"end":549130,"confidence":0.9873047,"speaker":"A"},{"text":"is","start":549130,"end":549290,"confidence":0.99365234,"speaker":"A"},{"text":"it","start":549290,"end":549450,"confidence":0.99902344,"speaker":"A"},{"text":"just","start":549450,"end":549650,"confidence":1,"speaker":"A"},{"text":"you?","start":549650,"end":549970,"confidence":0.9995117,"speaker":"A"},{"text":"It","start":551330,"end":551610,"confidence":0.95751953,"speaker":"B"},{"text":"looks","start":551610,"end":551810,"confidence":1,"speaker":"B"},{"text":"like","start":551810,"end":551930,"confidence":0.9995117,"speaker":"B"},{"text":"it's","start":551930,"end":552130,"confidence":0.9996745,"speaker":"B"},{"text":"just","start":552130,"end":552290,"confidence":1,"speaker":"B"},{"text":"me.","start":552290,"end":552570,"confidence":1,"speaker":"B"},{"text":"Josh","start":552570,"end":553010,"confidence":0.9995117,"speaker":"B"},{"text":"is","start":553010,"end":553290,"confidence":0.9970703,"speaker":"B"},{"text":"trying","start":553290,"end":553530,"confidence":0.9995117,"speaker":"B"},{"text":"to","start":553530,"end":553650,"confidence":1,"speaker":"B"},{"text":"get","start":553650,"end":553810,"confidence":1,"speaker":"B"},{"text":"in,","start":553810,"end":554010,"confidence":0.9995117,"speaker":"B"},{"text":"but","start":554010,"end":554170,"confidence":0.9995117,"speaker":"B"},{"text":"he's","start":554170,"end":554610,"confidence":0.92529297,"speaker":"B"},{"text":"trying","start":554610,"end":554930,"confidence":0.9995117,"speaker":"B"},{"text":"to","start":554930,"end":555090,"confidence":1,"speaker":"B"},{"text":"get","start":555090,"end":555210,"confidence":1,"speaker":"B"},{"text":"on","start":555210,"end":555490,"confidence":0.9272461,"speaker":"B"},{"text":"on","start":555650,"end":555970,"confidence":1,"speaker":"B"},{"text":"his","start":555970,"end":556210,"confidence":0.99902344,"speaker":"B"},{"text":"mobile","start":556210,"end":556530,"confidence":0.9998372,"speaker":"B"},{"text":"device","start":556530,"end":556810,"confidence":1,"speaker":"B"},{"text":"and","start":556810,"end":557010,"confidence":0.90478516,"speaker":"B"},{"text":"I","start":557010,"end":557210,"confidence":1,"speaker":"B"},{"text":"don't","start":557210,"end":557490,"confidence":0.98828125,"speaker":"B"},{"text":"think","start":557490,"end":557689,"confidence":1,"speaker":"B"},{"text":"that's","start":557689,"end":558010,"confidence":1,"speaker":"B"},{"text":"possible","start":558010,"end":558290,"confidence":1,"speaker":"B"},{"text":"with","start":558290,"end":558570,"confidence":0.9995117,"speaker":"B"},{"text":"Riverside.","start":558570,"end":559250,"confidence":0.9998372,"speaker":"B"},{"text":"Surprised?","start":563250,"end":563890,"confidence":0.9345703,"speaker":"A"},{"text":"I","start":564690,"end":564970,"confidence":0.9897461,"speaker":"A"},{"text":"mean,","start":564970,"end":565090,"confidence":0.99902344,"speaker":"A"},{"text":"I","start":565090,"end":565210,"confidence":0.99902344,"speaker":"A"},{"text":"know","start":565210,"end":565370,"confidence":1,"speaker":"A"},{"text":"they","start":565370,"end":565530,"confidence":1,"speaker":"A"},{"text":"have","start":565530,"end":565690,"confidence":1,"speaker":"A"},{"text":"an","start":565690,"end":565850,"confidence":0.99902344,"speaker":"A"},{"text":"app.","start":565850,"end":566130,"confidence":0.9863281,"speaker":"A"},{"text":"Maybe","start":567590,"end":567790,"confidence":0.93359375,"speaker":"B"},{"text":"he's","start":567790,"end":567990,"confidence":0.9996745,"speaker":"B"},{"text":"using.","start":567990,"end":568190,"confidence":0.99902344,"speaker":"B"},{"text":"I'm","start":568190,"end":568430,"confidence":0.99934894,"speaker":"B"},{"text":"not","start":568430,"end":568510,"confidence":0.99902344,"speaker":"B"},{"text":"sure","start":568510,"end":568630,"confidence":1,"speaker":"B"},{"text":"if","start":568630,"end":568710,"confidence":0.9980469,"speaker":"B"},{"text":"he's","start":568710,"end":568790,"confidence":0.9189453,"speaker":"B"},{"text":"using.","start":568790,"end":569030,"confidence":0.98535156,"speaker":"B"},{"text":"Using","start":569110,"end":569430,"confidence":1,"speaker":"B"},{"text":"the","start":569430,"end":569630,"confidence":0.99902344,"speaker":"B"},{"text":"app","start":569630,"end":569790,"confidence":0.9995117,"speaker":"B"},{"text":"or","start":569790,"end":569910,"confidence":0.9995117,"speaker":"B"},{"text":"not.","start":569910,"end":570070,"confidence":0.9995117,"speaker":"B"},{"text":"Okay.","start":570070,"end":570550,"confidence":0.99820966,"speaker":"A"},{"text":"Should","start":575190,"end":575470,"confidence":0.99658203,"speaker":"A"},{"text":"I","start":575470,"end":575630,"confidence":0.8354492,"speaker":"A"},{"text":"just","start":575630,"end":575910,"confidence":1,"speaker":"A"},{"text":"go?","start":575910,"end":576310,"confidence":1,"speaker":"A"},{"text":"Sure.","start":578230,"end":578630,"confidence":1,"speaker":"B"},{"text":"Okay.","start":579830,"end":580470,"confidence":0.91015625,"speaker":"A"},{"text":"Well,","start":582390,"end":582710,"confidence":0.9980469,"speaker":"A"},{"text":"thanks","start":582710,"end":583030,"confidence":0.9926758,"speaker":"A"},{"text":"for","start":583030,"end":583230,"confidence":1,"speaker":"A"},{"text":"joining","start":583230,"end":583549,"confidence":0.75911456,"speaker":"A"},{"text":"me,","start":583549,"end":583830,"confidence":0.99902344,"speaker":"A"},{"text":"Evan.","start":583830,"end":584310,"confidence":0.9511719,"speaker":"A"},{"text":"I","start":584310,"end":584510,"confidence":0.9995117,"speaker":"A"},{"text":"really","start":584510,"end":584670,"confidence":0.9995117,"speaker":"A"},{"text":"appreciate","start":584670,"end":584990,"confidence":0.9088135,"speaker":"A"},{"text":"it.","start":584990,"end":585270,"confidence":0.99853516,"speaker":"A"},{"text":"I","start":587430,"end":587670,"confidence":0.8666992,"speaker":"A"},{"text":"would","start":587670,"end":587790,"confidence":0.67871094,"speaker":"A"},{"text":"say","start":587790,"end":588070,"confidence":0.9448242,"speaker":"A"},{"text":"no.","start":588390,"end":588630,"confidence":0.9951172,"speaker":"A"},{"text":"I","start":588630,"end":588710,"confidence":0.9995117,"speaker":"A"},{"text":"mean","start":588710,"end":588830,"confidence":0.95947266,"speaker":"A"},{"text":"I","start":588830,"end":588990,"confidence":0.99902344,"speaker":"A"},{"text":"do,","start":588990,"end":589270,"confidence":1,"speaker":"A"},{"text":"seriously.","start":589270,"end":589910,"confidence":0.99934894,"speaker":"A"},{"text":"So","start":591830,"end":592110,"confidence":0.9995117,"speaker":"A"},{"text":"yeah,","start":592110,"end":592470,"confidence":1,"speaker":"A"},{"text":"this","start":592630,"end":592910,"confidence":0.99902344,"speaker":"A"},{"text":"is","start":592910,"end":593030,"confidence":0.79296875,"speaker":"A"},{"text":"a","start":593030,"end":593150,"confidence":0.6645508,"speaker":"A"},{"text":"kind","start":593150,"end":593310,"confidence":0.99853516,"speaker":"A"},{"text":"of","start":593310,"end":593430,"confidence":0.9995117,"speaker":"A"},{"text":"a","start":593430,"end":593550,"confidence":0.99609375,"speaker":"A"},{"text":"dry","start":593550,"end":593830,"confidence":0.8828125,"speaker":"A"},{"text":"run.","start":593830,"end":594150,"confidence":0.9970703,"speaker":"A"},{"text":"I","start":594710,"end":594830,"confidence":0.9941406,"speaker":"A"},{"text":"would","start":594830,"end":594950,"confidence":0.9980469,"speaker":"A"},{"text":"say","start":594950,"end":595070,"confidence":0.99560547,"speaker":"A"},{"text":"I'm","start":595070,"end":595270,"confidence":0.99869794,"speaker":"A"},{"text":"about","start":595270,"end":595470,"confidence":0.9995117,"speaker":"A"},{"text":"60%","start":595470,"end":596110,"confidence":0.92505,"speaker":"A"},{"text":"done","start":596110,"end":596350,"confidence":0.9995117,"speaker":"A"},{"text":"with","start":596350,"end":596510,"confidence":1,"speaker":"A"},{"text":"this","start":596510,"end":596710,"confidence":0.99853516,"speaker":"A"},{"text":"presentation","start":596710,"end":597350,"confidence":1,"speaker":"A"},{"text":"about","start":599270,"end":599670,"confidence":0.9975586,"speaker":"A"},{"text":"CloudKit","start":600310,"end":600990,"confidence":0.7687988,"speaker":"A"},{"text":"on","start":600990,"end":601150,"confidence":0.9980469,"speaker":"A"},{"text":"the","start":601150,"end":601310,"confidence":0.9946289,"speaker":"A"},{"text":"server","start":601310,"end":601750,"confidence":0.7963867,"speaker":"A"},{"text":"and","start":604070,"end":604470,"confidence":0.9892578,"speaker":"A"},{"text":"we'll","start":604870,"end":605230,"confidence":0.9514974,"speaker":"A"},{"text":"probably","start":605230,"end":605470,"confidence":1,"speaker":"A"},{"text":"hop","start":605470,"end":605710,"confidence":0.9946289,"speaker":"A"},{"text":"back","start":605710,"end":605950,"confidence":0.9995117,"speaker":"A"},{"text":"and","start":605950,"end":606110,"confidence":1,"speaker":"A"},{"text":"forth","start":606110,"end":606350,"confidence":1,"speaker":"A"},{"text":"between","start":606350,"end":606630,"confidence":1,"speaker":"A"},{"text":"Keynote","start":606630,"end":607230,"confidence":0.88049316,"speaker":"A"},{"text":"and","start":607230,"end":607390,"confidence":0.9975586,"speaker":"A"},{"text":"not","start":607390,"end":607590,"confidence":0.9458008,"speaker":"A"},{"text":"Keynote,","start":607590,"end":608310,"confidence":0.99328613,"speaker":"A"},{"text":"but","start":608870,"end":609270,"confidence":0.9941406,"speaker":"A"},{"text":"yeah.","start":609510,"end":609990,"confidence":0.9737956,"speaker":"A"},{"text":"So","start":611670,"end":611950,"confidence":0.9946289,"speaker":"A"},{"text":"this","start":611950,"end":612110,"confidence":0.9995117,"speaker":"A"},{"text":"is","start":612110,"end":612310,"confidence":0.9995117,"speaker":"A"},{"text":"CloudKit","start":612310,"end":612910,"confidence":0.92456055,"speaker":"A"},{"text":"as","start":612910,"end":613070,"confidence":0.9863281,"speaker":"A"},{"text":"your","start":613070,"end":613230,"confidence":0.94628906,"speaker":"A"},{"text":"backend","start":613230,"end":613750,"confidence":0.8310547,"speaker":"A"},{"text":"from","start":613910,"end":614310,"confidence":1,"speaker":"A"},{"text":"iOS","start":614310,"end":614870,"confidence":0.9992676,"speaker":"A"},{"text":"to","start":615030,"end":615390,"confidence":0.9941406,"speaker":"A"},{"text":"server","start":615390,"end":615830,"confidence":0.9873047,"speaker":"A"},{"text":"side","start":615830,"end":616070,"confidence":0.5727539,"speaker":"A"},{"text":"Swift.","start":616070,"end":616630,"confidence":0.9953613,"speaker":"A"},{"text":"So","start":627600,"end":627840,"confidence":0.9916992,"speaker":"A"},{"text":"what","start":628160,"end":628480,"confidence":0.9995117,"speaker":"A"},{"text":"is","start":628480,"end":628720,"confidence":0.99902344,"speaker":"A"},{"text":"CloudKit?","start":628720,"end":629440,"confidence":0.88281,"speaker":"A"},{"text":"CloudKit","start":629600,"end":630320,"confidence":0.88281,"speaker":"A"},{"text":"is","start":630320,"end":630600,"confidence":0.9921875,"speaker":"A"},{"text":"a","start":630600,"end":630880,"confidence":0.99853516,"speaker":"A"},{"text":"service","start":630880,"end":631200,"confidence":0.9995117,"speaker":"A"},{"text":"launched","start":632240,"end":632680,"confidence":0.99731445,"speaker":"A"},{"text":"by","start":632680,"end":632840,"confidence":1,"speaker":"A"},{"text":"Apple","start":632840,"end":633360,"confidence":1,"speaker":"A"},{"text":"probably","start":633600,"end":634000,"confidence":0.99869794,"speaker":"A"},{"text":"a","start":634000,"end":634160,"confidence":0.9995117,"speaker":"A"},{"text":"decade","start":634160,"end":634520,"confidence":0.99975586,"speaker":"A"},{"text":"ago","start":634520,"end":634800,"confidence":0.9995117,"speaker":"A"},{"text":"to","start":635920,"end":636279,"confidence":0.9848633,"speaker":"A"},{"text":"kind","start":636279,"end":636520,"confidence":0.8803711,"speaker":"A"},{"text":"of","start":636520,"end":636800,"confidence":0.98828125,"speaker":"A"},{"text":"give","start":636960,"end":637360,"confidence":0.9995117,"speaker":"A"},{"text":"developers","start":638880,"end":639680,"confidence":0.9995117,"speaker":"A"},{"text":"a","start":639840,"end":640200,"confidence":0.99902344,"speaker":"A"},{"text":"built","start":640200,"end":640520,"confidence":0.9995117,"speaker":"A"},{"text":"in","start":640520,"end":640720,"confidence":0.99316406,"speaker":"A"},{"text":"back","start":640720,"end":641000,"confidence":0.9995117,"speaker":"A"},{"text":"end","start":641000,"end":641280,"confidence":0.58935547,"speaker":"A"},{"text":"for","start":641280,"end":641520,"confidence":0.99609375,"speaker":"A"},{"text":"storing","start":641520,"end":641960,"confidence":0.9946289,"speaker":"A"},{"text":"data","start":641960,"end":642240,"confidence":0.99902344,"speaker":"A"},{"text":"for","start":642640,"end":642920,"confidence":0.9995117,"speaker":"A"},{"text":"their","start":642920,"end":643160,"confidence":0.99853516,"speaker":"A"},{"text":"apps.","start":643160,"end":643680,"confidence":0.99902344,"speaker":"A"},{"text":"One","start":644480,"end":644760,"confidence":0.9995117,"speaker":"A"},{"text":"of","start":644760,"end":644880,"confidence":0.9995117,"speaker":"A"},{"text":"the","start":644880,"end":645000,"confidence":0.99853516,"speaker":"A"},{"text":"biggest","start":645000,"end":645360,"confidence":1,"speaker":"A"},{"text":"benefits","start":645360,"end":646000,"confidence":0.99902344,"speaker":"A"},{"text":"is","start":646080,"end":646300,"confidence":0.84765625,"speaker":"A"},{"text":"is","start":646450,"end":646690,"confidence":0.9736328,"speaker":"A"},{"text":"how","start":646690,"end":647090,"confidence":0.9995117,"speaker":"A"},{"text":"cheap","start":647090,"end":647450,"confidence":0.9998372,"speaker":"A"},{"text":"it","start":647450,"end":647610,"confidence":0.9980469,"speaker":"A"},{"text":"is","start":647610,"end":647890,"confidence":0.9980469,"speaker":"A"},{"text":"to","start":647970,"end":648250,"confidence":0.99853516,"speaker":"A"},{"text":"use","start":648250,"end":648490,"confidence":0.9970703,"speaker":"A"},{"text":"for","start":648490,"end":648810,"confidence":0.9995117,"speaker":"A"},{"text":"iOS","start":648810,"end":649290,"confidence":0.9992676,"speaker":"A"},{"text":"developers.","start":649290,"end":649970,"confidence":0.998291,"speaker":"A"},{"text":"So","start":652450,"end":652850,"confidence":0.95751953,"speaker":"A"},{"text":"if","start":653570,"end":653850,"confidence":0.99853516,"speaker":"A"},{"text":"you","start":653850,"end":654130,"confidence":1,"speaker":"A"},{"text":"have","start":654450,"end":654850,"confidence":0.99902344,"speaker":"A"},{"text":"built","start":655330,"end":655690,"confidence":0.99934894,"speaker":"A"},{"text":"an","start":655690,"end":655850,"confidence":0.99560547,"speaker":"A"},{"text":"app,","start":655850,"end":656130,"confidence":0.9995117,"speaker":"A"},{"text":"you","start":656290,"end":656570,"confidence":1,"speaker":"A"},{"text":"could","start":656570,"end":656730,"confidence":0.6508789,"speaker":"A"},{"text":"just","start":656730,"end":656930,"confidence":0.99902344,"speaker":"A"},{"text":"add","start":656930,"end":657250,"confidence":0.99853516,"speaker":"A"},{"text":"CloudKit","start":657410,"end":658290,"confidence":0.89294,"speaker":"A"},{"text":"right","start":658290,"end":658610,"confidence":0.99853516,"speaker":"A"},{"text":"here","start":658610,"end":658930,"confidence":0.9995117,"speaker":"A"},{"text":"within","start":659570,"end":659970,"confidence":0.9975586,"speaker":"A"},{"text":"the","start":661330,"end":661730,"confidence":0.9970703,"speaker":"A"},{"text":"Xcode","start":662209,"end":662770,"confidence":0.91137695,"speaker":"A"},{"text":"project","start":662770,"end":663090,"confidence":1,"speaker":"A"},{"text":"and","start":663490,"end":663890,"confidence":0.9975586,"speaker":"A"},{"text":"use","start":665330,"end":665690,"confidence":0.99853516,"speaker":"A"},{"text":"the","start":665690,"end":665970,"confidence":0.9995117,"speaker":"A"},{"text":"regular","start":665970,"end":666370,"confidence":0.9995117,"speaker":"A"},{"text":"CloudKit","start":666370,"end":666970,"confidence":0.9975586,"speaker":"A"},{"text":"API","start":666970,"end":667490,"confidence":0.99853516,"speaker":"A"},{"text":"in","start":667890,"end":668170,"confidence":0.5913086,"speaker":"A"},{"text":"Swift","start":668170,"end":668570,"confidence":0.9951172,"speaker":"A"},{"text":"to","start":668570,"end":668810,"confidence":0.99902344,"speaker":"A"},{"text":"go","start":668810,"end":668970,"confidence":0.9975586,"speaker":"A"},{"text":"ahead","start":668970,"end":669250,"confidence":0.9765625,"speaker":"A"},{"text":"and","start":669250,"end":669530,"confidence":0.99902344,"speaker":"A"},{"text":"start","start":669530,"end":669730,"confidence":1,"speaker":"A"},{"text":"using","start":669730,"end":669930,"confidence":1,"speaker":"A"},{"text":"it","start":669930,"end":670130,"confidence":0.9980469,"speaker":"A"},{"text":"in","start":670130,"end":670330,"confidence":0.99902344,"speaker":"A"},{"text":"your","start":670330,"end":670530,"confidence":1,"speaker":"A"},{"text":"app.","start":670530,"end":670850,"confidence":0.9975586,"speaker":"A"},{"text":"Here","start":673390,"end":673630,"confidence":0.9946289,"speaker":"A"},{"text":"is","start":673630,"end":674030,"confidence":0.9995117,"speaker":"A"},{"text":"what","start":674030,"end":674430,"confidence":0.9995117,"speaker":"A"},{"text":"it","start":674430,"end":674750,"confidence":0.9980469,"speaker":"A"},{"text":"looks","start":674750,"end":675110,"confidence":1,"speaker":"A"},{"text":"like","start":675110,"end":675390,"confidence":0.99902344,"speaker":"A"},{"text":"to","start":675390,"end":675750,"confidence":0.99902344,"speaker":"A"},{"text":"create","start":675750,"end":675990,"confidence":0.9995117,"speaker":"A"},{"text":"a","start":675990,"end":676110,"confidence":0.9868164,"speaker":"A"},{"text":"new","start":676110,"end":676270,"confidence":0.99853516,"speaker":"A"},{"text":"record","start":676270,"end":676590,"confidence":0.9995117,"speaker":"A"},{"text":"type.","start":676590,"end":676990,"confidence":0.99194336,"speaker":"A"},{"text":"You","start":676990,"end":677150,"confidence":0.9995117,"speaker":"A"},{"text":"can","start":677150,"end":677270,"confidence":1,"speaker":"A"},{"text":"do","start":677270,"end":677430,"confidence":1,"speaker":"A"},{"text":"all","start":677430,"end":677590,"confidence":0.9995117,"speaker":"A"},{"text":"this","start":677590,"end":677870,"confidence":0.99853516,"speaker":"A"},{"text":"through","start":677870,"end":678270,"confidence":1,"speaker":"A"},{"text":"the","start":678430,"end":678790,"confidence":0.99902344,"speaker":"A"},{"text":"CloudKit","start":678790,"end":679510,"confidence":0.9987793,"speaker":"A"},{"text":"dashboard.","start":679510,"end":680190,"confidence":0.99938965,"speaker":"A"},{"text":"In","start":684190,"end":684470,"confidence":0.7402344,"speaker":"A"},{"text":"CloudKit","start":684470,"end":685150,"confidence":0.9477539,"speaker":"A"},{"text":"you","start":685390,"end":685670,"confidence":0.9995117,"speaker":"A"},{"text":"could","start":685670,"end":685830,"confidence":0.8930664,"speaker":"A"},{"text":"also","start":685830,"end":686030,"confidence":0.9995117,"speaker":"A"},{"text":"do","start":686030,"end":686230,"confidence":1,"speaker":"A"},{"text":"this","start":686230,"end":686470,"confidence":1,"speaker":"A"},{"text":"using","start":686470,"end":686830,"confidence":1,"speaker":"A"},{"text":"a","start":687150,"end":687430,"confidence":0.94921875,"speaker":"A"},{"text":"schema","start":687430,"end":687910,"confidence":0.9895833,"speaker":"A"},{"text":"file","start":687910,"end":688270,"confidence":0.8520508,"speaker":"A"},{"text":"too.","start":688670,"end":689070,"confidence":0.8598633,"speaker":"A"},{"text":"And","start":689390,"end":689670,"confidence":0.99316406,"speaker":"A"},{"text":"you","start":689670,"end":689830,"confidence":0.98583984,"speaker":"A"},{"text":"can","start":689830,"end":689990,"confidence":0.6220703,"speaker":"A"},{"text":"export","start":689990,"end":690310,"confidence":0.9975586,"speaker":"A"},{"text":"and","start":690310,"end":690470,"confidence":0.9692383,"speaker":"A"},{"text":"import","start":690470,"end":690750,"confidence":0.9970703,"speaker":"A"},{"text":"your","start":690830,"end":691150,"confidence":0.99902344,"speaker":"A"},{"text":"schema","start":691150,"end":691710,"confidence":0.92041016,"speaker":"A"},{"text":"that","start":691710,"end":692030,"confidence":0.99658203,"speaker":"A"},{"text":"way.","start":692030,"end":692350,"confidence":0.9975586,"speaker":"A"},{"text":"And","start":693230,"end":693630,"confidence":0.98046875,"speaker":"A"},{"text":"it's","start":693630,"end":694070,"confidence":0.9996745,"speaker":"A"},{"text":"not","start":694070,"end":694350,"confidence":0.9980469,"speaker":"A"},{"text":"a","start":694590,"end":694870,"confidence":0.9321289,"speaker":"A"},{"text":"SQL","start":694870,"end":695190,"confidence":0.9423828,"speaker":"A"},{"text":"based","start":695190,"end":695430,"confidence":0.99902344,"speaker":"A"},{"text":"database,","start":695430,"end":696030,"confidence":0.9995117,"speaker":"A"},{"text":"it's","start":696030,"end":696270,"confidence":0.97802734,"speaker":"A"},{"text":"much","start":696270,"end":696470,"confidence":0.9980469,"speaker":"A"},{"text":"more,","start":696470,"end":696830,"confidence":0.9892578,"speaker":"A"},{"text":"no","start":697310,"end":697670,"confidence":0.9902344,"speaker":"A"},{"text":"sequel","start":697670,"end":698110,"confidence":0.8517253,"speaker":"A"},{"text":"ish","start":698110,"end":698430,"confidence":0.9033203,"speaker":"A"},{"text":"or","start":698430,"end":698630,"confidence":0.9995117,"speaker":"A"},{"text":"an","start":698630,"end":698830,"confidence":0.9770508,"speaker":"A"},{"text":"abstract","start":698830,"end":699350,"confidence":0.9822591,"speaker":"A"},{"text":"layer","start":699350,"end":699910,"confidence":0.99886066,"speaker":"A"},{"text":"above","start":699910,"end":700230,"confidence":0.98461914,"speaker":"A"},{"text":"it.","start":700230,"end":700510,"confidence":0.99609375,"speaker":"A"},{"text":"But","start":701400,"end":701560,"confidence":0.99658203,"speaker":"A"},{"text":"essentially","start":701560,"end":702240,"confidence":0.97021484,"speaker":"A"},{"text":"you","start":702240,"end":702600,"confidence":0.9995117,"speaker":"A"},{"text":"can","start":702680,"end":703080,"confidence":0.9995117,"speaker":"A"},{"text":"create","start":703080,"end":703440,"confidence":0.9970703,"speaker":"A"},{"text":"records","start":703440,"end":704120,"confidence":0.99658203,"speaker":"A"},{"text":"kind","start":704520,"end":704800,"confidence":0.99658203,"speaker":"A"},{"text":"of","start":704800,"end":704920,"confidence":0.9970703,"speaker":"A"},{"text":"like","start":704920,"end":705040,"confidence":0.9995117,"speaker":"A"},{"text":"a","start":705040,"end":705200,"confidence":0.9995117,"speaker":"A"},{"text":"table","start":705200,"end":705480,"confidence":0.9995117,"speaker":"A"},{"text":"but","start":705480,"end":705680,"confidence":0.99902344,"speaker":"A"},{"text":"not","start":705680,"end":705880,"confidence":0.99853516,"speaker":"A"},{"text":"quite","start":705880,"end":706280,"confidence":0.99853516,"speaker":"A"},{"text":"in","start":707000,"end":707280,"confidence":0.98339844,"speaker":"A"},{"text":"your","start":707280,"end":707520,"confidence":0.9970703,"speaker":"A"},{"text":"records.","start":707520,"end":708200,"confidence":0.9963379,"speaker":"A"},{"text":"You","start":709400,"end":709680,"confidence":0.99902344,"speaker":"A"},{"text":"can","start":709680,"end":709960,"confidence":0.9995117,"speaker":"A"},{"text":"create","start":710360,"end":710760,"confidence":0.9824219,"speaker":"A"},{"text":"a","start":711400,"end":711760,"confidence":0.9980469,"speaker":"A"},{"text":"struct","start":711760,"end":712240,"confidence":0.83862305,"speaker":"A"},{"text":"for","start":712240,"end":712480,"confidence":0.99902344,"speaker":"A"},{"text":"it.","start":712480,"end":712680,"confidence":0.9980469,"speaker":"A"},{"text":"You","start":712680,"end":712880,"confidence":0.9995117,"speaker":"A"},{"text":"can","start":712880,"end":713040,"confidence":0.9995117,"speaker":"A"},{"text":"just","start":713040,"end":713240,"confidence":1,"speaker":"A"},{"text":"use","start":713240,"end":713560,"confidence":0.99902344,"speaker":"A"},{"text":"CloudKit","start":713960,"end":714600,"confidence":0.982666,"speaker":"A"},{"text":"directly","start":714600,"end":715120,"confidence":0.9995117,"speaker":"A"},{"text":"to","start":715120,"end":715360,"confidence":0.9995117,"speaker":"A"},{"text":"go","start":715360,"end":715520,"confidence":0.9995117,"speaker":"A"},{"text":"ahead","start":715520,"end":715800,"confidence":0.99853516,"speaker":"A"},{"text":"and","start":716440,"end":716760,"confidence":0.9951172,"speaker":"A"},{"text":"then","start":716760,"end":717039,"confidence":0.99072266,"speaker":"A"},{"text":"you","start":717039,"end":717280,"confidence":0.98535156,"speaker":"A"},{"text":"can","start":717280,"end":717480,"confidence":0.88964844,"speaker":"A"},{"text":"then","start":717480,"end":717760,"confidence":0.78759766,"speaker":"A"},{"text":"plug","start":717760,"end":718080,"confidence":0.9995117,"speaker":"A"},{"text":"it","start":718080,"end":718240,"confidence":0.99902344,"speaker":"A"},{"text":"into","start":718240,"end":718440,"confidence":0.99902344,"speaker":"A"},{"text":"your","start":718440,"end":718680,"confidence":0.9995117,"speaker":"A"},{"text":"app","start":718680,"end":718920,"confidence":0.9995117,"speaker":"A"},{"text":"and","start":718920,"end":719240,"confidence":0.9628906,"speaker":"A"},{"text":"do","start":719240,"end":719520,"confidence":0.9995117,"speaker":"A"},{"text":"fun","start":719520,"end":719760,"confidence":0.99853516,"speaker":"A"},{"text":"stuff","start":719760,"end":720040,"confidence":1,"speaker":"A"},{"text":"like","start":720040,"end":720200,"confidence":0.9995117,"speaker":"A"},{"text":"this.","start":720200,"end":720520,"confidence":0.9946289,"speaker":"A"},{"text":"We","start":721560,"end":721880,"confidence":0.44580078,"speaker":"A"},{"text":"can","start":721880,"end":722080,"confidence":0.9995117,"speaker":"A"},{"text":"do","start":722080,"end":722240,"confidence":1,"speaker":"A"},{"text":"things","start":722240,"end":722440,"confidence":1,"speaker":"A"},{"text":"like","start":722440,"end":722760,"confidence":0.9995117,"speaker":"A"},{"text":"queries","start":722840,"end":723520,"confidence":0.9477539,"speaker":"A"},{"text":"and","start":723520,"end":723880,"confidence":0.8354492,"speaker":"A"},{"text":"basic","start":724840,"end":725280,"confidence":0.99975586,"speaker":"A"},{"text":"database","start":725280,"end":725800,"confidence":0.99869794,"speaker":"A"},{"text":"stuff.","start":725800,"end":726200,"confidence":0.9996745,"speaker":"A"},{"text":"There's","start":726200,"end":726640,"confidence":0.99153644,"speaker":"A"},{"text":"a","start":726640,"end":726760,"confidence":0.99902344,"speaker":"A"},{"text":"lot","start":726760,"end":726840,"confidence":1,"speaker":"A"},{"text":"of","start":726840,"end":726960,"confidence":0.99902344,"speaker":"A"},{"text":"advantages","start":726960,"end":727520,"confidence":0.9991862,"speaker":"A"},{"text":"to","start":727520,"end":727760,"confidence":0.99853516,"speaker":"A"},{"text":"it.","start":727760,"end":728040,"confidence":0.99658203,"speaker":"A"},{"text":"For","start":729280,"end":729440,"confidence":0.9794922,"speaker":"A"},{"text":"one,","start":729440,"end":729760,"confidence":0.9667969,"speaker":"A"},{"text":"if","start":730080,"end":730400,"confidence":0.9995117,"speaker":"A"},{"text":"you're","start":730400,"end":730880,"confidence":0.95996094,"speaker":"A"},{"text":"doing","start":730960,"end":731360,"confidence":0.99902344,"speaker":"A"},{"text":"Apple","start":731840,"end":732320,"confidence":1,"speaker":"A"},{"text":"only,","start":732320,"end":732640,"confidence":0.9995117,"speaker":"A"},{"text":"then","start":733600,"end":734000,"confidence":0.99658203,"speaker":"A"},{"text":"it","start":734000,"end":734280,"confidence":0.9995117,"speaker":"A"},{"text":"definitely","start":734280,"end":734680,"confidence":0.99938965,"speaker":"A"},{"text":"makes","start":734680,"end":734880,"confidence":0.9980469,"speaker":"A"},{"text":"sense","start":734880,"end":735280,"confidence":0.99975586,"speaker":"A"},{"text":"to","start":735520,"end":735840,"confidence":0.99853516,"speaker":"A"},{"text":"look","start":735840,"end":736120,"confidence":0.98046875,"speaker":"A"},{"text":"into,","start":736120,"end":736440,"confidence":0.53515625,"speaker":"A"},{"text":"at","start":736440,"end":736640,"confidence":0.9995117,"speaker":"A"},{"text":"least","start":736640,"end":736800,"confidence":0.9995117,"speaker":"A"},{"text":"look","start":736800,"end":737040,"confidence":0.99902344,"speaker":"A"},{"text":"into","start":737040,"end":737320,"confidence":0.99902344,"speaker":"A"},{"text":"CloudKit.","start":737320,"end":738080,"confidence":0.9995117,"speaker":"A"},{"text":"If","start":742320,"end":742600,"confidence":0.9980469,"speaker":"A"},{"text":"you're","start":742600,"end":742800,"confidence":0.9996745,"speaker":"A"},{"text":"just","start":742800,"end":742920,"confidence":0.9995117,"speaker":"A"},{"text":"going","start":742920,"end":743040,"confidence":0.92333984,"speaker":"A"},{"text":"to","start":743040,"end":743120,"confidence":0.99902344,"speaker":"A"},{"text":"deploy","start":743120,"end":743480,"confidence":1,"speaker":"A"},{"text":"to","start":743480,"end":743840,"confidence":0.99316406,"speaker":"A"},{"text":"Apple","start":744480,"end":744960,"confidence":0.99975586,"speaker":"A"},{"text":"Devices.","start":744960,"end":745440,"confidence":1,"speaker":"A"},{"text":"If","start":746080,"end":746440,"confidence":0.9995117,"speaker":"A"},{"text":"you","start":746440,"end":746800,"confidence":0.9995117,"speaker":"A"},{"text":"don't","start":747120,"end":747560,"confidence":0.9637044,"speaker":"A"},{"text":"mind","start":747560,"end":747920,"confidence":0.9995117,"speaker":"A"},{"text":"the,","start":748320,"end":748720,"confidence":0.9042969,"speaker":"A"},{"text":"the","start":749920,"end":750200,"confidence":0.9995117,"speaker":"A"},{"text":"fact","start":750200,"end":750360,"confidence":0.9995117,"speaker":"A"},{"text":"that","start":750360,"end":750520,"confidence":1,"speaker":"A"},{"text":"it's","start":750520,"end":750720,"confidence":0.9996745,"speaker":"A"},{"text":"not","start":750720,"end":750920,"confidence":0.84814453,"speaker":"A"},{"text":"a","start":750920,"end":751160,"confidence":0.5908203,"speaker":"A"},{"text":"regular","start":751160,"end":751560,"confidence":0.9992676,"speaker":"A"},{"text":"SQL","start":751560,"end":751960,"confidence":0.98860675,"speaker":"A"},{"text":"database,","start":751960,"end":752640,"confidence":0.9998372,"speaker":"A"},{"text":"that's","start":754050,"end":754210,"confidence":0.9980469,"speaker":"A"},{"text":"something","start":754210,"end":754410,"confidence":0.9995117,"speaker":"A"},{"text":"too","start":754410,"end":754650,"confidence":0.68408203,"speaker":"A"},{"text":"to","start":754650,"end":754810,"confidence":0.99853516,"speaker":"A"},{"text":"think","start":754810,"end":754930,"confidence":1,"speaker":"A"},{"text":"about.","start":754930,"end":755090,"confidence":0.9995117,"speaker":"A"},{"text":"If","start":755090,"end":755290,"confidence":0.9995117,"speaker":"A"},{"text":"you","start":755290,"end":755450,"confidence":1,"speaker":"A"},{"text":"like","start":755450,"end":755610,"confidence":0.92333984,"speaker":"A"},{"text":"need","start":755610,"end":755770,"confidence":0.9848633,"speaker":"A"},{"text":"a","start":755770,"end":755890,"confidence":0.9926758,"speaker":"A"},{"text":"SQL","start":755890,"end":756210,"confidence":0.96533203,"speaker":"A"},{"text":"database,","start":756210,"end":756650,"confidence":0.98063153,"speaker":"A"},{"text":"this","start":756650,"end":756850,"confidence":0.97998047,"speaker":"A"},{"text":"might","start":756850,"end":757050,"confidence":1,"speaker":"A"},{"text":"not","start":757050,"end":757210,"confidence":0.9995117,"speaker":"A"},{"text":"be","start":757210,"end":757490,"confidence":1,"speaker":"A"},{"text":"what","start":757730,"end":758050,"confidence":0.9819336,"speaker":"A"},{"text":"you","start":758050,"end":758370,"confidence":0.9995117,"speaker":"A"},{"text":"want.","start":758370,"end":758770,"confidence":0.9926758,"speaker":"A"},{"text":"And","start":759410,"end":759690,"confidence":0.95654297,"speaker":"A"},{"text":"then","start":759690,"end":759890,"confidence":0.9819336,"speaker":"A"},{"text":"if","start":759890,"end":760050,"confidence":1,"speaker":"A"},{"text":"you","start":760050,"end":760170,"confidence":1,"speaker":"A"},{"text":"don't","start":760170,"end":760370,"confidence":1,"speaker":"A"},{"text":"mind","start":760370,"end":760530,"confidence":1,"speaker":"A"},{"text":"working","start":760530,"end":760770,"confidence":1,"speaker":"A"},{"text":"with","start":760770,"end":761010,"confidence":0.9848633,"speaker":"A"},{"text":"a","start":761010,"end":761170,"confidence":0.99902344,"speaker":"A"},{"text":"lot","start":761170,"end":761290,"confidence":1,"speaker":"A"},{"text":"of","start":761290,"end":761410,"confidence":0.9995117,"speaker":"A"},{"text":"the","start":761410,"end":761530,"confidence":0.9995117,"speaker":"A"},{"text":"abstraction","start":761530,"end":762130,"confidence":0.9991455,"speaker":"A"},{"text":"layers","start":762130,"end":762610,"confidence":0.99934894,"speaker":"A"},{"text":"that","start":763010,"end":763330,"confidence":0.99853516,"speaker":"A"},{"text":"CloudKit","start":763330,"end":763970,"confidence":0.99902344,"speaker":"A"},{"text":"provides,","start":763970,"end":764610,"confidence":0.9995117,"speaker":"A"},{"text":"then","start":766930,"end":767330,"confidence":0.99658203,"speaker":"A"},{"text":"this","start":767650,"end":767970,"confidence":0.9995117,"speaker":"A"},{"text":"might","start":767970,"end":768170,"confidence":0.99609375,"speaker":"A"},{"text":"be","start":768170,"end":768370,"confidence":1,"speaker":"A"},{"text":"good","start":768370,"end":768530,"confidence":1,"speaker":"A"},{"text":"for","start":768530,"end":768650,"confidence":0.87402344,"speaker":"A"},{"text":"you","start":768650,"end":768850,"confidence":1,"speaker":"A"},{"text":"to","start":768850,"end":769050,"confidence":1,"speaker":"A"},{"text":"get","start":769050,"end":769210,"confidence":1,"speaker":"A"},{"text":"started","start":769210,"end":769490,"confidence":0.9995117,"speaker":"A"},{"text":"or","start":770050,"end":770410,"confidence":0.99658203,"speaker":"A"},{"text":"especially","start":770410,"end":770730,"confidence":0.9995117,"speaker":"A"},{"text":"if","start":770730,"end":770930,"confidence":1,"speaker":"A"},{"text":"you","start":770930,"end":771050,"confidence":1,"speaker":"A"},{"text":"don't","start":771050,"end":771250,"confidence":0.9995117,"speaker":"A"},{"text":"have","start":771250,"end":771370,"confidence":1,"speaker":"A"},{"text":"any","start":771370,"end":771570,"confidence":0.9995117,"speaker":"A"},{"text":"database","start":771570,"end":772130,"confidence":0.9998372,"speaker":"A"},{"text":"experience.","start":772130,"end":772450,"confidence":0.9995117,"speaker":"A"},{"text":"So","start":774130,"end":774410,"confidence":0.99316406,"speaker":"A"},{"text":"as","start":774410,"end":774570,"confidence":0.9995117,"speaker":"A"},{"text":"far","start":774570,"end":774730,"confidence":1,"speaker":"A"},{"text":"as","start":774730,"end":774930,"confidence":1,"speaker":"A"},{"text":"like","start":774930,"end":775250,"confidence":0.9770508,"speaker":"A"},{"text":"server","start":775570,"end":776090,"confidence":0.99975586,"speaker":"A"},{"text":"choices,","start":776090,"end":776650,"confidence":0.98291016,"speaker":"A"},{"text":"I","start":776650,"end":776850,"confidence":0.9995117,"speaker":"A"},{"text":"would","start":776850,"end":777010,"confidence":1,"speaker":"A"},{"text":"say","start":777010,"end":777290,"confidence":1,"speaker":"A"},{"text":"CloudKit","start":777290,"end":777970,"confidence":0.9926758,"speaker":"A"},{"text":"might","start":777970,"end":778170,"confidence":0.99365234,"speaker":"A"},{"text":"not","start":778170,"end":778330,"confidence":0.57714844,"speaker":"A"},{"text":"be","start":778330,"end":778490,"confidence":1,"speaker":"A"},{"text":"your","start":778490,"end":778690,"confidence":1,"speaker":"A"},{"text":"first","start":778690,"end":778930,"confidence":0.9995117,"speaker":"A"},{"text":"choice,","start":778930,"end":779330,"confidence":0.99975586,"speaker":"A"},{"text":"but","start":779970,"end":780090,"confidence":0.9970703,"speaker":"A"},{"text":"it","start":780090,"end":780250,"confidence":0.99902344,"speaker":"A"},{"text":"certainly","start":780250,"end":780610,"confidence":1,"speaker":"A"},{"text":"is","start":780610,"end":780930,"confidence":1,"speaker":"A"},{"text":"a","start":780930,"end":781210,"confidence":0.9995117,"speaker":"A"},{"text":"decent","start":781210,"end":781570,"confidence":1,"speaker":"A"},{"text":"choice","start":781570,"end":781970,"confidence":0.99975586,"speaker":"A"},{"text":"if","start":782290,"end":782610,"confidence":0.6225586,"speaker":"A"},{"text":"you're","start":782610,"end":782890,"confidence":0.9943034,"speaker":"A"},{"text":"going","start":782890,"end":783090,"confidence":0.99853516,"speaker":"A"},{"text":"the","start":783090,"end":783290,"confidence":0.9145508,"speaker":"A"},{"text":"Apple","start":783290,"end":783650,"confidence":0.9995117,"speaker":"A"},{"text":"only","start":783650,"end":783970,"confidence":0.9995117,"speaker":"A"},{"text":"route.","start":783970,"end":784450,"confidence":0.9938965,"speaker":"A"},{"text":"But","start":789970,"end":790250,"confidence":0.99658203,"speaker":"A"},{"text":"then","start":790250,"end":790410,"confidence":1,"speaker":"A"},{"text":"the","start":790410,"end":790530,"confidence":1,"speaker":"A"},{"text":"question","start":790530,"end":790730,"confidence":1,"speaker":"A"},{"text":"comes","start":790730,"end":791010,"confidence":0.9951172,"speaker":"A"},{"text":"in,","start":791010,"end":791250,"confidence":0.97216797,"speaker":"A"},{"text":"why","start":791250,"end":791450,"confidence":1,"speaker":"A"},{"text":"would","start":791450,"end":791610,"confidence":1,"speaker":"A"},{"text":"you","start":791610,"end":791770,"confidence":1,"speaker":"A"},{"text":"want","start":791770,"end":792010,"confidence":0.99902344,"speaker":"A"},{"text":"Cloud","start":792010,"end":792450,"confidence":0.954834,"speaker":"A"},{"text":"server","start":792450,"end":792850,"confidence":0.98461914,"speaker":"A"},{"text":"side","start":792850,"end":793050,"confidence":0.55859375,"speaker":"A"},{"text":"CloudKit?","start":793050,"end":793730,"confidence":0.98095703,"speaker":"A"},{"text":"Why","start":793890,"end":794170,"confidence":1,"speaker":"A"},{"text":"would","start":794170,"end":794330,"confidence":1,"speaker":"A"},{"text":"you","start":794330,"end":794490,"confidence":1,"speaker":"A"},{"text":"want","start":794490,"end":794610,"confidence":0.9941406,"speaker":"A"},{"text":"to","start":794610,"end":794690,"confidence":0.9995117,"speaker":"A"},{"text":"do","start":794690,"end":794810,"confidence":1,"speaker":"A"},{"text":"anything","start":794810,"end":795090,"confidence":1,"speaker":"A"},{"text":"with","start":795090,"end":795250,"confidence":0.9995117,"speaker":"A"},{"text":"CloudKit","start":795250,"end":795810,"confidence":0.9885254,"speaker":"A"},{"text":"on","start":795810,"end":796009,"confidence":0.9980469,"speaker":"A"},{"text":"the","start":796009,"end":796170,"confidence":0.9995117,"speaker":"A"},{"text":"server?","start":796170,"end":796610,"confidence":1,"speaker":"A"},{"text":"So","start":797970,"end":798250,"confidence":0.99316406,"speaker":"A"},{"text":"here's,","start":798250,"end":798610,"confidence":0.9793294,"speaker":"A"},{"text":"here's","start":798610,"end":799090,"confidence":0.9996745,"speaker":"A"},{"text":"the","start":799250,"end":799530,"confidence":0.9995117,"speaker":"A"},{"text":"first","start":799530,"end":799810,"confidence":0.9995117,"speaker":"A"},{"text":"case.","start":799890,"end":800290,"confidence":0.9995117,"speaker":"A"},{"text":"Well,","start":800690,"end":801090,"confidence":0.96533203,"speaker":"A"},{"text":"this","start":801250,"end":801530,"confidence":0.9995117,"speaker":"A"},{"text":"is","start":801530,"end":801690,"confidence":1,"speaker":"A"},{"text":"how","start":801690,"end":801890,"confidence":1,"speaker":"A"},{"text":"you","start":801890,"end":802090,"confidence":1,"speaker":"A"},{"text":"can","start":802090,"end":802290,"confidence":0.9995117,"speaker":"A"},{"text":"go","start":802290,"end":802490,"confidence":0.9995117,"speaker":"A"},{"text":"ahead","start":802490,"end":802650,"confidence":0.9995117,"speaker":"A"},{"text":"and","start":802650,"end":802850,"confidence":0.97216797,"speaker":"A"},{"text":"do","start":802850,"end":803050,"confidence":1,"speaker":"A"},{"text":"that","start":803050,"end":803250,"confidence":0.99902344,"speaker":"A"},{"text":"is","start":803250,"end":803570,"confidence":0.90234375,"speaker":"A"},{"text":"they","start":803970,"end":804330,"confidence":0.99902344,"speaker":"A"},{"text":"provide","start":804330,"end":804690,"confidence":1,"speaker":"A"},{"text":"actually","start":804690,"end":805050,"confidence":0.9980469,"speaker":"A"},{"text":"a","start":805050,"end":805290,"confidence":0.91259766,"speaker":"A"},{"text":"REST","start":805290,"end":805490,"confidence":0.9995117,"speaker":"A"},{"text":"API","start":805490,"end":806090,"confidence":0.95166016,"speaker":"A"},{"text":"for","start":806090,"end":806450,"confidence":0.9946289,"speaker":"A"},{"text":"calls","start":806450,"end":806930,"confidence":0.9992676,"speaker":"A"},{"text":"to","start":806930,"end":807170,"confidence":0.9970703,"speaker":"A"},{"text":"CloudKit","start":807170,"end":807880,"confidence":0.9848633,"speaker":"A"},{"text":"using","start":808910,"end":809150,"confidence":0.95654297,"speaker":"A"},{"text":"the,","start":809310,"end":809710,"confidence":0.98828125,"speaker":"A"},{"text":"if","start":809950,"end":810230,"confidence":0.99902344,"speaker":"A"},{"text":"you","start":810230,"end":810350,"confidence":1,"speaker":"A"},{"text":"go","start":810350,"end":810430,"confidence":0.9995117,"speaker":"A"},{"text":"to","start":810430,"end":810550,"confidence":0.99853516,"speaker":"A"},{"text":"the","start":810550,"end":810670,"confidence":0.9995117,"speaker":"A"},{"text":"documentation,","start":810670,"end":811350,"confidence":0.99902344,"speaker":"A"},{"text":"I'll","start":811350,"end":811670,"confidence":0.99820966,"speaker":"A"},{"text":"provide","start":811670,"end":811910,"confidence":0.99658203,"speaker":"A"},{"text":"a","start":811910,"end":812110,"confidence":0.9067383,"speaker":"A"},{"text":"link","start":812110,"end":812350,"confidence":0.9992676,"speaker":"A"},{"text":"to","start":812350,"end":812550,"confidence":0.9995117,"speaker":"A"},{"text":"that","start":812550,"end":812830,"confidence":0.8276367,"speaker":"A"},{"text":"CloudKit","start":812910,"end":813590,"confidence":0.87280273,"speaker":"A"},{"text":"Web","start":813590,"end":813830,"confidence":0.99658203,"speaker":"A"},{"text":"Services","start":813830,"end":814110,"confidence":0.9995117,"speaker":"A"},{"text":"which","start":815310,"end":815710,"confidence":0.99902344,"speaker":"A"},{"text":"provides","start":816510,"end":816990,"confidence":0.99975586,"speaker":"A"},{"text":"a","start":816990,"end":817070,"confidence":0.9995117,"speaker":"A"},{"text":"lot","start":817070,"end":817190,"confidence":1,"speaker":"A"},{"text":"of","start":817190,"end":817310,"confidence":0.9995117,"speaker":"A"},{"text":"the","start":817310,"end":817430,"confidence":0.9980469,"speaker":"A"},{"text":"documentation","start":817430,"end":818070,"confidence":0.9998047,"speaker":"A"},{"text":"for","start":818070,"end":818270,"confidence":0.9995117,"speaker":"A"},{"text":"what","start":818270,"end":818390,"confidence":0.99902344,"speaker":"A"},{"text":"we'll","start":818390,"end":818630,"confidence":0.8699544,"speaker":"A"},{"text":"be","start":818630,"end":818790,"confidence":1,"speaker":"A"},{"text":"talking","start":818790,"end":819030,"confidence":0.97631836,"speaker":"A"},{"text":"about","start":819030,"end":819230,"confidence":0.9995117,"speaker":"A"},{"text":"today.","start":819230,"end":819550,"confidence":0.99902344,"speaker":"A"},{"text":"A","start":820910,"end":821150,"confidence":0.99658203,"speaker":"A"},{"text":"lot","start":821150,"end":821270,"confidence":1,"speaker":"A"},{"text":"of","start":821270,"end":821430,"confidence":0.9995117,"speaker":"A"},{"text":"this","start":821430,"end":821590,"confidence":0.99902344,"speaker":"A"},{"text":"is","start":821590,"end":821790,"confidence":0.99853516,"speaker":"A"},{"text":"abstracted","start":821790,"end":822390,"confidence":0.88964844,"speaker":"A"},{"text":"out","start":822390,"end":822550,"confidence":0.99902344,"speaker":"A"},{"text":"in","start":822550,"end":822670,"confidence":0.9980469,"speaker":"A"},{"text":"the","start":822670,"end":822750,"confidence":0.9995117,"speaker":"A"},{"text":"JavaScript","start":822750,"end":823350,"confidence":0.99698895,"speaker":"A"},{"text":"library.","start":823350,"end":823790,"confidence":0.9916992,"speaker":"A"},{"text":"So","start":823870,"end":824109,"confidence":0.9838867,"speaker":"A"},{"text":"if","start":824109,"end":824230,"confidence":0.99902344,"speaker":"A"},{"text":"you","start":824230,"end":824350,"confidence":1,"speaker":"A"},{"text":"want","start":824350,"end":824510,"confidence":0.95166016,"speaker":"A"},{"text":"to","start":824510,"end":824670,"confidence":0.9980469,"speaker":"A"},{"text":"do","start":824670,"end":824790,"confidence":0.9995117,"speaker":"A"},{"text":"stuff","start":824790,"end":824990,"confidence":1,"speaker":"A"},{"text":"on","start":824990,"end":825110,"confidence":0.9995117,"speaker":"A"},{"text":"a","start":825110,"end":825270,"confidence":0.98828125,"speaker":"A"},{"text":"website,","start":825270,"end":825550,"confidence":0.99609375,"speaker":"A"},{"text":"they","start":826430,"end":826790,"confidence":0.9995117,"speaker":"A"},{"text":"provide","start":826790,"end":827150,"confidence":1,"speaker":"A"},{"text":"a","start":827230,"end":827630,"confidence":0.99853516,"speaker":"A"},{"text":"CloudKit","start":827790,"end":828590,"confidence":0.99438477,"speaker":"A"},{"text":"JavaScript","start":828590,"end":829390,"confidence":0.9239909,"speaker":"A"},{"text":"library","start":830270,"end":830830,"confidence":0.9996745,"speaker":"A"},{"text":"for","start":830830,"end":831110,"confidence":0.99853516,"speaker":"A"},{"text":"that.","start":831110,"end":831470,"confidence":0.99609375,"speaker":"A"},{"text":"Sorry,","start":833150,"end":833710,"confidence":0.8925781,"speaker":"A"},{"text":"just","start":836190,"end":836310,"confidence":0.93847656,"speaker":"A"},{"text":"going","start":836310,"end":836510,"confidence":0.9814453,"speaker":"A"},{"text":"into","start":836510,"end":836790,"confidence":0.9121094,"speaker":"A"},{"text":"do","start":836790,"end":837030,"confidence":0.99560547,"speaker":"A"},{"text":"not","start":837030,"end":837230,"confidence":0.99902344,"speaker":"A"},{"text":"disturb","start":837230,"end":837870,"confidence":0.87369794,"speaker":"A"},{"text":"mode.","start":838670,"end":839230,"confidence":0.73999023,"speaker":"A"},{"text":"They","start":847950,"end":848270,"confidence":0.9404297,"speaker":"A"},{"text":"even","start":848270,"end":848590,"confidence":0.7373047,"speaker":"A"},{"text":"in","start":848750,"end":849030,"confidence":0.9995117,"speaker":"A"},{"text":"that","start":849030,"end":849270,"confidence":0.99902344,"speaker":"A"},{"text":"web","start":849270,"end":849710,"confidence":0.9995117,"speaker":"A"},{"text":"references","start":849790,"end":850429,"confidence":0.9367676,"speaker":"A"},{"text":"documentation","start":850430,"end":851070,"confidence":0.97734374,"speaker":"A"},{"text":"they","start":851070,"end":851270,"confidence":0.9980469,"speaker":"A"},{"text":"provide","start":851270,"end":851510,"confidence":1,"speaker":"A"},{"text":"a","start":851510,"end":851710,"confidence":0.8413086,"speaker":"A"},{"text":"composing","start":851710,"end":852150,"confidence":0.92008466,"speaker":"A"},{"text":"web","start":852150,"end":852390,"confidence":0.998291,"speaker":"A"},{"text":"service","start":852390,"end":852630,"confidence":0.99902344,"speaker":"A"},{"text":"request","start":852630,"end":853150,"confidence":0.9995117,"speaker":"A"},{"text":"and","start":853470,"end":853750,"confidence":0.9970703,"speaker":"A"},{"text":"all","start":853750,"end":853910,"confidence":0.9995117,"speaker":"A"},{"text":"these","start":853910,"end":854110,"confidence":0.99902344,"speaker":"A"},{"text":"instructions","start":854110,"end":854670,"confidence":0.9996745,"speaker":"A"},{"text":"about","start":854670,"end":854910,"confidence":1,"speaker":"A"},{"text":"how","start":854910,"end":855070,"confidence":0.9995117,"speaker":"A"},{"text":"to","start":855070,"end":855190,"confidence":1,"speaker":"A"},{"text":"go","start":855190,"end":855310,"confidence":1,"speaker":"A"},{"text":"ahead","start":855310,"end":855470,"confidence":0.9995117,"speaker":"A"},{"text":"and","start":855470,"end":855670,"confidence":1,"speaker":"A"},{"text":"do","start":855670,"end":855830,"confidence":1,"speaker":"A"},{"text":"that.","start":855830,"end":856110,"confidence":1,"speaker":"A"},{"text":"So","start":857470,"end":857870,"confidence":0.98876953,"speaker":"A"},{"text":"man,","start":858270,"end":858590,"confidence":0.9482422,"speaker":"A"},{"text":"was","start":858590,"end":858790,"confidence":0.99853516,"speaker":"A"},{"text":"it","start":858790,"end":858950,"confidence":0.9277344,"speaker":"A"},{"text":"like","start":858950,"end":859110,"confidence":0.9941406,"speaker":"A"},{"text":"half","start":859110,"end":859310,"confidence":0.9995117,"speaker":"A"},{"text":"a","start":859310,"end":859470,"confidence":0.99902344,"speaker":"A"},{"text":"decade","start":859470,"end":859790,"confidence":0.99975586,"speaker":"A"},{"text":"ago","start":859790,"end":860110,"confidence":1,"speaker":"A"},{"text":"that","start":860880,"end":861120,"confidence":0.97216797,"speaker":"A"},{"text":"I","start":861280,"end":861680,"confidence":0.97314453,"speaker":"A"},{"text":"built","start":862960,"end":863320,"confidence":0.99153644,"speaker":"A"},{"text":"Heart","start":863320,"end":863520,"confidence":0.8129883,"speaker":"A"},{"text":"Twitch","start":863520,"end":864000,"confidence":0.98999023,"speaker":"A"},{"text":"and","start":864480,"end":864880,"confidence":0.9814453,"speaker":"A"},{"text":"at","start":865360,"end":865640,"confidence":0.99902344,"speaker":"A"},{"text":"the","start":865640,"end":865840,"confidence":0.99853516,"speaker":"A"},{"text":"time","start":865840,"end":866080,"confidence":1,"speaker":"A"},{"text":"I","start":866080,"end":866280,"confidence":1,"speaker":"A"},{"text":"don't","start":866280,"end":866520,"confidence":0.99934894,"speaker":"A"},{"text":"think","start":866520,"end":866720,"confidence":1,"speaker":"A"},{"text":"there","start":866720,"end":866960,"confidence":0.99365234,"speaker":"A"},{"text":"was","start":866960,"end":867280,"confidence":0.9995117,"speaker":"A"},{"text":"anything,","start":867440,"end":868080,"confidence":0.99975586,"speaker":"A"},{"text":"there","start":870080,"end":870360,"confidence":0.99658203,"speaker":"A"},{"text":"was","start":870360,"end":870560,"confidence":0.99902344,"speaker":"A"},{"text":"anything","start":870560,"end":870960,"confidence":0.99975586,"speaker":"A"},{"text":"like","start":870960,"end":871200,"confidence":0.99902344,"speaker":"A"},{"text":"sign","start":871200,"end":871440,"confidence":0.99658203,"speaker":"A"},{"text":"in","start":871440,"end":871640,"confidence":0.9819336,"speaker":"A"},{"text":"with","start":871640,"end":871800,"confidence":1,"speaker":"A"},{"text":"Apple","start":871800,"end":872160,"confidence":0.9995117,"speaker":"A"},{"text":"even.","start":872160,"end":872480,"confidence":0.9970703,"speaker":"A"},{"text":"And","start":872880,"end":873280,"confidence":0.97265625,"speaker":"A"},{"text":"like","start":873520,"end":873840,"confidence":0.9399414,"speaker":"A"},{"text":"I","start":873840,"end":874160,"confidence":0.9995117,"speaker":"A"},{"text":"really","start":874160,"end":874560,"confidence":0.99902344,"speaker":"A"},{"text":"didn't","start":875120,"end":875640,"confidence":0.99348956,"speaker":"A"},{"text":"want","start":875640,"end":875920,"confidence":0.9995117,"speaker":"A"},{"text":"like","start":876880,"end":877280,"confidence":0.9794922,"speaker":"A"},{"text":"to","start":878160,"end":878480,"confidence":0.98291016,"speaker":"A"},{"text":"explain","start":878480,"end":878760,"confidence":0.99853516,"speaker":"A"},{"text":"how","start":878760,"end":878920,"confidence":0.9995117,"speaker":"A"},{"text":"harshwitch","start":878920,"end":879520,"confidence":0.62939453,"speaker":"A"},{"text":"works","start":879520,"end":879800,"confidence":0.99975586,"speaker":"A"},{"text":"is","start":879800,"end":879960,"confidence":0.91064453,"speaker":"A"},{"text":"you","start":879960,"end":880120,"confidence":0.99853516,"speaker":"A"},{"text":"have","start":880120,"end":880320,"confidence":1,"speaker":"A"},{"text":"like","start":880320,"end":880520,"confidence":0.9902344,"speaker":"A"},{"text":"a","start":880520,"end":880680,"confidence":0.9995117,"speaker":"A"},{"text":"watch","start":880680,"end":880960,"confidence":0.99902344,"speaker":"A"},{"text":"and","start":881360,"end":881720,"confidence":0.6225586,"speaker":"A"},{"text":"it","start":881720,"end":881960,"confidence":0.9995117,"speaker":"A"},{"text":"will","start":881960,"end":882200,"confidence":0.9995117,"speaker":"A"},{"text":"send","start":882200,"end":882600,"confidence":0.9291992,"speaker":"A"},{"text":"the","start":882600,"end":882840,"confidence":0.9995117,"speaker":"A"},{"text":"heart","start":882840,"end":883040,"confidence":0.9995117,"speaker":"A"},{"text":"rate","start":883040,"end":883280,"confidence":0.9995117,"speaker":"A"},{"text":"to","start":883280,"end":883480,"confidence":1,"speaker":"A"},{"text":"the","start":883480,"end":883640,"confidence":1,"speaker":"A"},{"text":"server","start":883640,"end":884160,"confidence":0.99975586,"speaker":"A"},{"text":"and","start":885360,"end":885640,"confidence":0.9921875,"speaker":"A"},{"text":"then","start":885640,"end":885920,"confidence":0.9926758,"speaker":"A"},{"text":"the","start":887020,"end":887180,"confidence":0.99658203,"speaker":"A"},{"text":"server","start":887180,"end":887580,"confidence":1,"speaker":"A"},{"text":"will","start":887580,"end":887780,"confidence":0.99902344,"speaker":"A"},{"text":"then","start":887780,"end":888020,"confidence":0.9995117,"speaker":"A"},{"text":"use","start":888020,"end":888260,"confidence":1,"speaker":"A"},{"text":"a","start":888260,"end":888420,"confidence":0.99853516,"speaker":"A"},{"text":"web","start":888420,"end":888660,"confidence":0.7871094,"speaker":"A"},{"text":"socket","start":888660,"end":889180,"confidence":0.9996745,"speaker":"A"},{"text":"to","start":889180,"end":889540,"confidence":0.9995117,"speaker":"A"},{"text":"push","start":889540,"end":889860,"confidence":1,"speaker":"A"},{"text":"it","start":889860,"end":890020,"confidence":0.99902344,"speaker":"A"},{"text":"out","start":890020,"end":890180,"confidence":0.9995117,"speaker":"A"},{"text":"to","start":890180,"end":890340,"confidence":1,"speaker":"A"},{"text":"a","start":890340,"end":890500,"confidence":0.99853516,"speaker":"A"},{"text":"web","start":890500,"end":890740,"confidence":0.99975586,"speaker":"A"},{"text":"page.","start":890740,"end":891100,"confidence":0.84643555,"speaker":"A"},{"text":"And","start":892060,"end":892340,"confidence":0.97558594,"speaker":"A"},{"text":"then","start":892340,"end":892620,"confidence":0.9995117,"speaker":"A"},{"text":"you","start":892620,"end":892900,"confidence":0.99902344,"speaker":"A"},{"text":"would","start":892900,"end":893180,"confidence":0.9838867,"speaker":"A"},{"text":"point","start":893500,"end":893900,"confidence":0.9926758,"speaker":"A"},{"text":"OBS","start":893980,"end":894380,"confidence":0.9897461,"speaker":"A"},{"text":"or","start":894540,"end":894780,"confidence":0.99072266,"speaker":"A"},{"text":"some","start":894780,"end":894900,"confidence":0.9995117,"speaker":"A"},{"text":"sort","start":894900,"end":895100,"confidence":0.9926758,"speaker":"A"},{"text":"of","start":895100,"end":895260,"confidence":0.53027344,"speaker":"A"},{"text":"streaming","start":895260,"end":895700,"confidence":0.91813153,"speaker":"A"},{"text":"software","start":895700,"end":896020,"confidence":0.9998779,"speaker":"A"},{"text":"to","start":896020,"end":896180,"confidence":0.9995117,"speaker":"A"},{"text":"the","start":896180,"end":896340,"confidence":1,"speaker":"A"},{"text":"URL","start":896340,"end":896860,"confidence":0.99487305,"speaker":"A"},{"text":"or","start":896860,"end":897060,"confidence":0.9980469,"speaker":"A"},{"text":"to","start":897060,"end":897220,"confidence":0.99609375,"speaker":"A"},{"text":"the","start":897220,"end":897340,"confidence":1,"speaker":"A"},{"text":"browser","start":897340,"end":897700,"confidence":0.9983724,"speaker":"A"},{"text":"window","start":897700,"end":898060,"confidence":1,"speaker":"A"},{"text":"and","start":898060,"end":898220,"confidence":0.99072266,"speaker":"A"},{"text":"then","start":898220,"end":898380,"confidence":0.8310547,"speaker":"A"},{"text":"that","start":898380,"end":898580,"confidence":0.9995117,"speaker":"A"},{"text":"way","start":898580,"end":898740,"confidence":0.9995117,"speaker":"A"},{"text":"you","start":898740,"end":898860,"confidence":1,"speaker":"A"},{"text":"can","start":898860,"end":898980,"confidence":0.9995117,"speaker":"A"},{"text":"stream","start":898980,"end":899260,"confidence":0.99609375,"speaker":"A"},{"text":"your","start":899260,"end":899460,"confidence":0.99853516,"speaker":"A"},{"text":"heart","start":899460,"end":899660,"confidence":0.9980469,"speaker":"A"},{"text":"rate.","start":899660,"end":899940,"confidence":0.9951172,"speaker":"A"},{"text":"That's","start":899940,"end":900220,"confidence":0.9996745,"speaker":"A"},{"text":"how","start":900220,"end":900300,"confidence":0.9995117,"speaker":"A"},{"text":"it","start":900300,"end":900420,"confidence":0.99853516,"speaker":"A"},{"text":"works.","start":900420,"end":900860,"confidence":0.9946289,"speaker":"A"},{"text":"And","start":901500,"end":901780,"confidence":0.9711914,"speaker":"A"},{"text":"what","start":901780,"end":901940,"confidence":0.9995117,"speaker":"A"},{"text":"I","start":901940,"end":902100,"confidence":1,"speaker":"A"},{"text":"really","start":902100,"end":902339,"confidence":0.9995117,"speaker":"A"},{"text":"didn't","start":902339,"end":902659,"confidence":0.9980469,"speaker":"A"},{"text":"want","start":902659,"end":902900,"confidence":1,"speaker":"A"},{"text":"is","start":902900,"end":903180,"confidence":0.99902344,"speaker":"A"},{"text":"a","start":903180,"end":903500,"confidence":0.9711914,"speaker":"A"},{"text":"difficult","start":903500,"end":903980,"confidence":0.9699707,"speaker":"A"},{"text":"way","start":903980,"end":904180,"confidence":0.9995117,"speaker":"A"},{"text":"for","start":904180,"end":904380,"confidence":0.9995117,"speaker":"A"},{"text":"a","start":904380,"end":904580,"confidence":0.8876953,"speaker":"A"},{"text":"user","start":904580,"end":904900,"confidence":1,"speaker":"A"},{"text":"to","start":904900,"end":905100,"confidence":0.9995117,"speaker":"A"},{"text":"log","start":905100,"end":905420,"confidence":1,"speaker":"A"},{"text":"in","start":905420,"end":905820,"confidence":0.9838867,"speaker":"A"},{"text":"with","start":906540,"end":906820,"confidence":0.9995117,"speaker":"A"},{"text":"a","start":906820,"end":906980,"confidence":0.7949219,"speaker":"A"},{"text":"username","start":906980,"end":907500,"confidence":0.9975586,"speaker":"A"},{"text":"and","start":907500,"end":907620,"confidence":0.99902344,"speaker":"A"},{"text":"password","start":907620,"end":908020,"confidence":0.90152997,"speaker":"A"},{"text":"on","start":908020,"end":908180,"confidence":0.6225586,"speaker":"A"},{"text":"the","start":908180,"end":908340,"confidence":0.9995117,"speaker":"A"},{"text":"watch","start":908340,"end":908620,"confidence":0.9995117,"speaker":"A"},{"text":"because","start":908620,"end":908900,"confidence":0.72558594,"speaker":"A"},{"text":"we","start":908900,"end":909020,"confidence":0.9995117,"speaker":"A"},{"text":"all","start":909020,"end":909140,"confidence":0.99902344,"speaker":"A"},{"text":"know","start":909140,"end":909300,"confidence":0.9980469,"speaker":"A"},{"text":"typing","start":909300,"end":909620,"confidence":0.8249512,"speaker":"A"},{"text":"on","start":909620,"end":909740,"confidence":0.9921875,"speaker":"A"},{"text":"the","start":909740,"end":909820,"confidence":0.9951172,"speaker":"A"},{"text":"watch","start":909820,"end":910020,"confidence":0.99902344,"speaker":"A"},{"text":"is","start":910020,"end":910380,"confidence":0.84472656,"speaker":"A"},{"text":"hell.","start":910780,"end":911260,"confidence":0.9157715,"speaker":"A"},{"text":"So","start":911900,"end":912300,"confidence":0.9770508,"speaker":"A"},{"text":"my,","start":912460,"end":912860,"confidence":0.70410156,"speaker":"A"},{"text":"my","start":912860,"end":913140,"confidence":0.9995117,"speaker":"A"},{"text":"thought","start":913140,"end":913340,"confidence":0.99902344,"speaker":"A"},{"text":"was","start":913340,"end":913620,"confidence":0.99853516,"speaker":"A"},{"text":"like,","start":913620,"end":913980,"confidence":0.9897461,"speaker":"A"},{"text":"and","start":914320,"end":914480,"confidence":0.6791992,"speaker":"A"},{"text":"I","start":914480,"end":914680,"confidence":1,"speaker":"A"},{"text":"didn't","start":914680,"end":914920,"confidence":0.9996745,"speaker":"A"},{"text":"have","start":914920,"end":915200,"confidence":0.9921875,"speaker":"A"},{"text":"sign","start":915280,"end":915600,"confidence":0.8886719,"speaker":"A"},{"text":"in","start":915600,"end":915800,"confidence":0.59814453,"speaker":"A"},{"text":"with","start":915800,"end":915960,"confidence":1,"speaker":"A"},{"text":"Apple,","start":915960,"end":916280,"confidence":1,"speaker":"A"},{"text":"right?","start":916280,"end":916560,"confidence":0.9970703,"speaker":"A"},{"text":"So","start":917440,"end":917720,"confidence":0.9995117,"speaker":"A"},{"text":"my","start":917720,"end":917880,"confidence":0.99902344,"speaker":"A"},{"text":"thought","start":917880,"end":918080,"confidence":0.9995117,"speaker":"A"},{"text":"was","start":918080,"end":918320,"confidence":0.99902344,"speaker":"A"},{"text":"why","start":918320,"end":918520,"confidence":1,"speaker":"A"},{"text":"don't","start":918520,"end":918720,"confidence":0.9972331,"speaker":"A"},{"text":"we","start":918720,"end":918840,"confidence":1,"speaker":"A"},{"text":"use","start":918840,"end":919000,"confidence":1,"speaker":"A"},{"text":"CloudKit?","start":919000,"end":919680,"confidence":0.9992676,"speaker":"A"},{"text":"Because","start":919840,"end":920120,"confidence":0.98095703,"speaker":"A"},{"text":"you're","start":920120,"end":920320,"confidence":0.9998372,"speaker":"A"},{"text":"already","start":920320,"end":920520,"confidence":1,"speaker":"A"},{"text":"signed","start":920520,"end":920880,"confidence":0.9963379,"speaker":"A"},{"text":"in","start":920880,"end":921000,"confidence":0.71728516,"speaker":"A"},{"text":"a","start":921000,"end":921120,"confidence":0.61376953,"speaker":"A"},{"text":"CloudKit","start":921120,"end":921640,"confidence":0.99658203,"speaker":"A"},{"text":"on","start":921640,"end":921800,"confidence":0.9995117,"speaker":"A"},{"text":"the","start":921800,"end":921960,"confidence":1,"speaker":"A"},{"text":"Watch","start":921960,"end":922240,"confidence":0.99853516,"speaker":"A"},{"text":"with","start":922800,"end":923120,"confidence":0.99853516,"speaker":"A"},{"text":"your,","start":923120,"end":923440,"confidence":0.9980469,"speaker":"A"},{"text":"your","start":923440,"end":923760,"confidence":0.9995117,"speaker":"A"},{"text":"id.","start":923760,"end":924080,"confidence":0.9995117,"speaker":"A"},{"text":"And","start":926640,"end":926920,"confidence":0.99316406,"speaker":"A"},{"text":"what","start":926920,"end":927080,"confidence":0.99902344,"speaker":"A"},{"text":"you","start":927080,"end":927320,"confidence":1,"speaker":"A"},{"text":"do","start":927320,"end":927680,"confidence":1,"speaker":"A"},{"text":"is","start":928320,"end":928720,"confidence":0.99902344,"speaker":"A"},{"text":"you","start":929440,"end":929720,"confidence":0.9995117,"speaker":"A"},{"text":"log","start":929720,"end":929920,"confidence":1,"speaker":"A"},{"text":"in","start":929920,"end":930159,"confidence":0.9975586,"speaker":"A"},{"text":"with","start":930159,"end":930359,"confidence":1,"speaker":"A"},{"text":"a","start":930359,"end":930480,"confidence":0.9794922,"speaker":"A"},{"text":"regular","start":930480,"end":930760,"confidence":1,"speaker":"A"},{"text":"like","start":930760,"end":930960,"confidence":0.9975586,"speaker":"A"},{"text":"email","start":930960,"end":931240,"confidence":1,"speaker":"A"},{"text":"address","start":931240,"end":931520,"confidence":1,"speaker":"A"},{"text":"and","start":931520,"end":931760,"confidence":0.6791992,"speaker":"A"},{"text":"password","start":931760,"end":932320,"confidence":0.88378906,"speaker":"A"},{"text":"in","start":933040,"end":933440,"confidence":0.7763672,"speaker":"A"},{"text":"Heart","start":933680,"end":934000,"confidence":0.66796875,"speaker":"A"},{"text":"Twitch","start":934000,"end":934400,"confidence":0.9975586,"speaker":"A"},{"text":"on","start":934400,"end":934560,"confidence":1,"speaker":"A"},{"text":"the","start":934560,"end":934680,"confidence":1,"speaker":"A"},{"text":"website.","start":934680,"end":934960,"confidence":0.9995117,"speaker":"A"},{"text":"And","start":935840,"end":936120,"confidence":0.99902344,"speaker":"A"},{"text":"then","start":936120,"end":936280,"confidence":0.9995117,"speaker":"A"},{"text":"there's","start":936280,"end":936520,"confidence":0.8927409,"speaker":"A"},{"text":"a","start":936520,"end":936640,"confidence":0.9995117,"speaker":"A"},{"text":"little,","start":936640,"end":936840,"confidence":1,"speaker":"A"},{"text":"there's","start":936840,"end":937200,"confidence":0.9996745,"speaker":"A"},{"text":"a","start":937200,"end":937360,"confidence":0.9995117,"speaker":"A"},{"text":"site,","start":937360,"end":937640,"confidence":0.9995117,"speaker":"A"},{"text":"there's","start":937640,"end":937960,"confidence":0.99886066,"speaker":"A"},{"text":"a","start":937960,"end":938160,"confidence":0.9995117,"speaker":"A"},{"text":"part","start":938160,"end":938360,"confidence":1,"speaker":"A"},{"text":"of","start":938360,"end":938480,"confidence":1,"speaker":"A"},{"text":"the","start":938480,"end":938560,"confidence":1,"speaker":"A"},{"text":"site","start":938560,"end":938720,"confidence":1,"speaker":"A"},{"text":"where","start":938720,"end":938920,"confidence":1,"speaker":"A"},{"text":"you","start":938920,"end":939040,"confidence":1,"speaker":"A"},{"text":"can","start":939040,"end":939280,"confidence":1,"speaker":"A"},{"text":"sign","start":939840,"end":940120,"confidence":1,"speaker":"A"},{"text":"into","start":940120,"end":940360,"confidence":0.8144531,"speaker":"A"},{"text":"CloudKit","start":940360,"end":941120,"confidence":0.9995117,"speaker":"A"},{"text":"and","start":942180,"end":942300,"confidence":0.94628906,"speaker":"A"},{"text":"then","start":942300,"end":942500,"confidence":0.99902344,"speaker":"A"},{"text":"from","start":942500,"end":942740,"confidence":1,"speaker":"A"},{"text":"there","start":942740,"end":943060,"confidence":0.9995117,"speaker":"A"},{"text":"you","start":944180,"end":944540,"confidence":0.9526367,"speaker":"A"},{"text":"can,","start":944540,"end":944900,"confidence":1,"speaker":"A"},{"text":"because,","start":945860,"end":946260,"confidence":0.8623047,"speaker":"A"},{"text":"because","start":946260,"end":946540,"confidence":0.99853516,"speaker":"A"},{"text":"of","start":946540,"end":946700,"confidence":0.9897461,"speaker":"A"},{"text":"the","start":946700,"end":946820,"confidence":0.9980469,"speaker":"A"},{"text":"CloudKit","start":946820,"end":947340,"confidence":0.99438477,"speaker":"A"},{"text":"JavaScript","start":947340,"end":947980,"confidence":0.9984538,"speaker":"A"},{"text":"library,","start":947980,"end":948380,"confidence":0.9995117,"speaker":"A"},{"text":"you","start":948380,"end":948540,"confidence":0.95751953,"speaker":"A"},{"text":"can","start":948540,"end":948660,"confidence":0.99902344,"speaker":"A"},{"text":"then","start":948660,"end":948820,"confidence":0.99853516,"speaker":"A"},{"text":"I","start":948820,"end":948980,"confidence":0.9951172,"speaker":"A"},{"text":"can","start":948980,"end":949100,"confidence":0.9975586,"speaker":"A"},{"text":"then","start":949100,"end":949300,"confidence":0.9951172,"speaker":"A"},{"text":"pull","start":949300,"end":949620,"confidence":0.99975586,"speaker":"A"},{"text":"the","start":949620,"end":949940,"confidence":0.9140625,"speaker":"A"},{"text":"all","start":952260,"end":952580,"confidence":0.99609375,"speaker":"A"},{"text":"the","start":952580,"end":952780,"confidence":0.99902344,"speaker":"A"},{"text":"devices","start":952780,"end":953220,"confidence":0.9992676,"speaker":"A"},{"text":"because","start":953220,"end":953540,"confidence":0.99902344,"speaker":"A"},{"text":"when","start":953540,"end":953740,"confidence":1,"speaker":"A"},{"text":"you","start":953740,"end":953900,"confidence":0.9995117,"speaker":"A"},{"text":"first","start":953900,"end":954100,"confidence":1,"speaker":"A"},{"text":"launch","start":954100,"end":954340,"confidence":1,"speaker":"A"},{"text":"the","start":954340,"end":954540,"confidence":0.9746094,"speaker":"A"},{"text":"app","start":954540,"end":954700,"confidence":0.9995117,"speaker":"A"},{"text":"on","start":954700,"end":954820,"confidence":0.9995117,"speaker":"A"},{"text":"the","start":954820,"end":954900,"confidence":0.9995117,"speaker":"A"},{"text":"Watch,","start":954900,"end":955100,"confidence":0.9897461,"speaker":"A"},{"text":"it","start":955100,"end":955340,"confidence":0.93408203,"speaker":"A"},{"text":"adds","start":955340,"end":955580,"confidence":0.9987793,"speaker":"A"},{"text":"your","start":955580,"end":955740,"confidence":0.9980469,"speaker":"A"},{"text":"watch","start":955740,"end":956020,"confidence":0.9995117,"speaker":"A"},{"text":"to","start":956340,"end":956620,"confidence":0.9995117,"speaker":"A"},{"text":"the","start":956620,"end":956740,"confidence":0.9995117,"speaker":"A"},{"text":"CloudKit","start":956740,"end":957300,"confidence":0.99609375,"speaker":"A"},{"text":"database.","start":957300,"end":957940,"confidence":0.9998372,"speaker":"A"},{"text":"And","start":958260,"end":958540,"confidence":0.9921875,"speaker":"A"},{"text":"then","start":958540,"end":958660,"confidence":0.9995117,"speaker":"A"},{"text":"I","start":958660,"end":958780,"confidence":0.99902344,"speaker":"A"},{"text":"could","start":958780,"end":958940,"confidence":0.66503906,"speaker":"A"},{"text":"pull","start":958940,"end":959140,"confidence":1,"speaker":"A"},{"text":"that","start":959140,"end":959300,"confidence":0.9975586,"speaker":"A"},{"text":"in","start":959300,"end":959540,"confidence":0.9980469,"speaker":"A"},{"text":"and","start":959540,"end":959740,"confidence":0.9995117,"speaker":"A"},{"text":"then","start":959740,"end":959900,"confidence":0.9970703,"speaker":"A"},{"text":"add","start":959900,"end":960060,"confidence":0.9995117,"speaker":"A"},{"text":"that","start":960060,"end":960220,"confidence":0.9995117,"speaker":"A"},{"text":"to","start":960220,"end":960380,"confidence":0.9995117,"speaker":"A"},{"text":"my","start":960380,"end":960540,"confidence":0.9995117,"speaker":"A"},{"text":"postgres","start":960540,"end":961140,"confidence":0.98583984,"speaker":"A"},{"text":"database.","start":961140,"end":961700,"confidence":1,"speaker":"A"},{"text":"So","start":961700,"end":961980,"confidence":0.99658203,"speaker":"A"},{"text":"then","start":961980,"end":962260,"confidence":0.9970703,"speaker":"A"},{"text":"there","start":962260,"end":962540,"confidence":1,"speaker":"A"},{"text":"is","start":962540,"end":962740,"confidence":0.9995117,"speaker":"A"},{"text":"no","start":962740,"end":962940,"confidence":0.9995117,"speaker":"A"},{"text":"need","start":962940,"end":963140,"confidence":1,"speaker":"A"},{"text":"for","start":963140,"end":963380,"confidence":0.9995117,"speaker":"A"},{"text":"authentication","start":963380,"end":964180,"confidence":0.9998779,"speaker":"A"},{"text":"because","start":964740,"end":965140,"confidence":0.9995117,"speaker":"A"},{"text":"I","start":965220,"end":965500,"confidence":0.9980469,"speaker":"A"},{"text":"already","start":965500,"end":965700,"confidence":1,"speaker":"A"},{"text":"have","start":965700,"end":965900,"confidence":0.99853516,"speaker":"A"},{"text":"the","start":965900,"end":966060,"confidence":0.9995117,"speaker":"A"},{"text":"CloudKit,","start":966060,"end":966740,"confidence":0.99975586,"speaker":"A"},{"text":"the","start":967720,"end":967880,"confidence":0.9663086,"speaker":"A"},{"text":"device","start":967880,"end":968280,"confidence":0.9992676,"speaker":"A"},{"text":"added","start":968280,"end":968600,"confidence":0.9995117,"speaker":"A"},{"text":"in","start":969000,"end":969280,"confidence":0.9995117,"speaker":"A"},{"text":"my","start":969280,"end":969480,"confidence":0.9926758,"speaker":"A"},{"text":"postgres","start":969480,"end":970000,"confidence":0.89941406,"speaker":"A"},{"text":"database.","start":970000,"end":970400,"confidence":0.9998372,"speaker":"A"},{"text":"So","start":970400,"end":970520,"confidence":0.8930664,"speaker":"A"},{"text":"it's","start":970520,"end":970720,"confidence":0.87093097,"speaker":"A"},{"text":"kind","start":970720,"end":970840,"confidence":0.93603516,"speaker":"A"},{"text":"of","start":970840,"end":970960,"confidence":0.859375,"speaker":"A"},{"text":"like","start":970960,"end":971120,"confidence":0.9736328,"speaker":"A"},{"text":"knows,","start":971120,"end":971440,"confidence":0.94555664,"speaker":"A"},{"text":"oh","start":971440,"end":971680,"confidence":0.97143555,"speaker":"A"},{"text":"yeah,","start":971680,"end":972040,"confidence":0.9983724,"speaker":"A"},{"text":"this","start":972200,"end":972480,"confidence":0.9995117,"speaker":"A"},{"text":"is","start":972480,"end":972720,"confidence":0.99902344,"speaker":"A"},{"text":"Leo's","start":972720,"end":973280,"confidence":0.9902344,"speaker":"A"},{"text":"watch,","start":973280,"end":973560,"confidence":0.99853516,"speaker":"A"},{"text":"he","start":974040,"end":974320,"confidence":0.99902344,"speaker":"A"},{"text":"doesn't","start":974320,"end":974520,"confidence":0.9996745,"speaker":"A"},{"text":"need","start":974520,"end":974640,"confidence":0.99902344,"speaker":"A"},{"text":"to","start":974640,"end":974840,"confidence":0.9863281,"speaker":"A"},{"text":"authenticate.","start":974840,"end":975520,"confidence":0.9996338,"speaker":"A"},{"text":"And","start":975520,"end":975760,"confidence":0.9116211,"speaker":"A"},{"text":"that","start":975760,"end":975920,"confidence":0.99365234,"speaker":"A"},{"text":"way","start":975920,"end":976120,"confidence":0.99853516,"speaker":"A"},{"text":"we","start":976120,"end":976320,"confidence":0.9995117,"speaker":"A"},{"text":"can","start":976320,"end":976520,"confidence":0.9995117,"speaker":"A"},{"text":"link","start":976520,"end":976800,"confidence":0.99975586,"speaker":"A"},{"text":"devices","start":976800,"end":977240,"confidence":0.9995117,"speaker":"A"},{"text":"to","start":977240,"end":977520,"confidence":0.9614258,"speaker":"A"},{"text":"accounts","start":977520,"end":978200,"confidence":0.9980469,"speaker":"A"},{"text":"without","start":978280,"end":978680,"confidence":0.9995117,"speaker":"A"},{"text":"having","start":978680,"end":978960,"confidence":0.9995117,"speaker":"A"},{"text":"to","start":978960,"end":979120,"confidence":0.9995117,"speaker":"A"},{"text":"do","start":979120,"end":979280,"confidence":0.9995117,"speaker":"A"},{"text":"any","start":979280,"end":979440,"confidence":0.9995117,"speaker":"A"},{"text":"sort","start":979440,"end":979640,"confidence":0.99625653,"speaker":"A"},{"text":"of","start":979640,"end":979760,"confidence":0.9951172,"speaker":"A"},{"text":"login","start":979760,"end":980200,"confidence":0.984375,"speaker":"A"},{"text":"process.","start":980200,"end":980520,"confidence":0.9995117,"speaker":"A"},{"text":"And","start":981080,"end":981360,"confidence":0.9008789,"speaker":"A"},{"text":"so","start":981360,"end":981600,"confidence":0.59228516,"speaker":"A"},{"text":"this","start":981600,"end":981840,"confidence":0.9995117,"speaker":"A"},{"text":"was","start":981840,"end":982000,"confidence":0.9951172,"speaker":"A"},{"text":"my","start":982000,"end":982200,"confidence":0.99902344,"speaker":"A"},{"text":"use","start":982200,"end":982440,"confidence":0.9916992,"speaker":"A"},{"text":"case","start":982440,"end":982760,"confidence":0.9995117,"speaker":"A"},{"text":"for","start":982919,"end":983320,"confidence":0.9995117,"speaker":"A"},{"text":"doing","start":983800,"end":984200,"confidence":0.99902344,"speaker":"A"},{"text":"server","start":985160,"end":985680,"confidence":0.71899414,"speaker":"A"},{"text":"side.","start":985680,"end":985960,"confidence":0.9086914,"speaker":"A"},{"text":"Essentially","start":986040,"end":986680,"confidence":0.9888916,"speaker":"A"},{"text":"CloudKit","start":987000,"end":987720,"confidence":0.87207,"speaker":"A"},{"text":"was","start":987720,"end":988000,"confidence":0.98583984,"speaker":"A"},{"text":"I","start":988000,"end":988240,"confidence":0.99902344,"speaker":"A"},{"text":"could","start":988240,"end":988400,"confidence":0.99365234,"speaker":"A"},{"text":"call","start":988400,"end":988600,"confidence":0.9995117,"speaker":"A"},{"text":"the","start":988600,"end":988800,"confidence":0.99853516,"speaker":"A"},{"text":"CloudKit","start":988800,"end":989360,"confidence":0.9609375,"speaker":"A"},{"text":"web","start":989360,"end":989560,"confidence":0.9902344,"speaker":"A"},{"text":"server","start":989560,"end":990040,"confidence":0.99902344,"speaker":"A"},{"text":"based","start":993410,"end":993610,"confidence":0.98876953,"speaker":"A"},{"text":"on","start":993610,"end":993850,"confidence":1,"speaker":"A"},{"text":"that","start":993850,"end":994050,"confidence":0.9995117,"speaker":"A"},{"text":"person's","start":994050,"end":994690,"confidence":0.99690753,"speaker":"A"},{"text":"web","start":995570,"end":995970,"confidence":0.9992676,"speaker":"A"},{"text":"authentication","start":995970,"end":996610,"confidence":0.9998779,"speaker":"A"},{"text":"token,","start":996610,"end":996970,"confidence":0.9998372,"speaker":"A"},{"text":"which","start":996970,"end":997130,"confidence":0.9995117,"speaker":"A"},{"text":"we'll","start":997130,"end":997330,"confidence":0.9316406,"speaker":"A"},{"text":"get","start":997330,"end":997490,"confidence":0.99902344,"speaker":"A"},{"text":"all","start":997490,"end":997730,"confidence":0.74365234,"speaker":"A"},{"text":"into","start":997730,"end":998010,"confidence":0.99072266,"speaker":"A"},{"text":"later.","start":998010,"end":998370,"confidence":0.99902344,"speaker":"A"},{"text":"I","start":998530,"end":998850,"confidence":0.5698242,"speaker":"A"},{"text":"then","start":998850,"end":999050,"confidence":0.91748047,"speaker":"A"},{"text":"pull","start":999050,"end":999250,"confidence":0.99975586,"speaker":"A"},{"text":"that","start":999250,"end":999410,"confidence":0.9980469,"speaker":"A"},{"text":"information","start":999410,"end":999730,"confidence":0.9995117,"speaker":"A"},{"text":"in.","start":999970,"end":1000370,"confidence":0.9824219,"speaker":"A"},{"text":"So.","start":1002050,"end":1002450,"confidence":0.8515625,"speaker":"A"},{"text":"Cool.","start":1007250,"end":1007730,"confidence":0.9333496,"speaker":"A"},{"text":"Just","start":1010770,"end":1011050,"confidence":0.99121094,"speaker":"A"},{"text":"checking","start":1011050,"end":1011370,"confidence":0.9980469,"speaker":"A"},{"text":"if","start":1011370,"end":1011530,"confidence":0.99853516,"speaker":"A"},{"text":"anybody's","start":1011530,"end":1012050,"confidence":0.94539386,"speaker":"A"},{"text":"having","start":1012050,"end":1012210,"confidence":0.9995117,"speaker":"A"},{"text":"issues.","start":1012210,"end":1012530,"confidence":0.99853516,"speaker":"A"},{"text":"It","start":1012530,"end":1012770,"confidence":0.5439453,"speaker":"A"},{"text":"doesn't","start":1012770,"end":1013050,"confidence":0.9983724,"speaker":"A"},{"text":"look","start":1013050,"end":1013210,"confidence":0.99902344,"speaker":"A"},{"text":"like","start":1013210,"end":1013370,"confidence":0.99853516,"speaker":"A"},{"text":"it.","start":1013370,"end":1013650,"confidence":0.9975586,"speaker":"A"},{"text":"So","start":1013650,"end":1014050,"confidence":0.8925781,"speaker":"A"},{"text":"that's","start":1014690,"end":1015050,"confidence":0.98014325,"speaker":"A"},{"text":"good","start":1015050,"end":1015210,"confidence":0.9995117,"speaker":"A"},{"text":"to","start":1015210,"end":1015370,"confidence":0.9980469,"speaker":"A"},{"text":"know.","start":1015370,"end":1015650,"confidence":0.9975586,"speaker":"A"},{"text":"So","start":1017170,"end":1017410,"confidence":0.9707031,"speaker":"A"},{"text":"that","start":1017410,"end":1017530,"confidence":0.98779297,"speaker":"A"},{"text":"was","start":1017530,"end":1017690,"confidence":0.9995117,"speaker":"A"},{"text":"the","start":1017690,"end":1017850,"confidence":0.9995117,"speaker":"A"},{"text":"private","start":1017850,"end":1018090,"confidence":0.9995117,"speaker":"A"},{"text":"database","start":1018090,"end":1018690,"confidence":0.9998372,"speaker":"A"},{"text":"piece,","start":1018690,"end":1019090,"confidence":0.99576825,"speaker":"A"},{"text":"but","start":1019950,"end":1020070,"confidence":0.97558594,"speaker":"A"},{"text":"I","start":1020070,"end":1020230,"confidence":0.99853516,"speaker":"A"},{"text":"actually","start":1020230,"end":1020470,"confidence":0.9970703,"speaker":"A"},{"text":"think","start":1020470,"end":1020790,"confidence":0.9995117,"speaker":"A"},{"text":"a","start":1020790,"end":1021030,"confidence":0.9921875,"speaker":"A"},{"text":"much","start":1021030,"end":1021230,"confidence":0.9946289,"speaker":"A"},{"text":"more","start":1021230,"end":1021470,"confidence":1,"speaker":"A"},{"text":"useful","start":1021470,"end":1021910,"confidence":0.99975586,"speaker":"A"},{"text":"case","start":1021910,"end":1022270,"confidence":0.9995117,"speaker":"A"},{"text":"would","start":1022670,"end":1022990,"confidence":1,"speaker":"A"},{"text":"be","start":1022990,"end":1023270,"confidence":1,"speaker":"A"},{"text":"the","start":1023270,"end":1023510,"confidence":0.9995117,"speaker":"A"},{"text":"public","start":1023510,"end":1023750,"confidence":0.9995117,"speaker":"A"},{"text":"database","start":1023750,"end":1024430,"confidence":0.99934894,"speaker":"A"},{"text":"because","start":1024990,"end":1025390,"confidence":0.9946289,"speaker":"A"},{"text":"the","start":1026830,"end":1027150,"confidence":0.99853516,"speaker":"A"},{"text":"idea","start":1027150,"end":1027550,"confidence":0.9758301,"speaker":"A"},{"text":"would","start":1027550,"end":1027750,"confidence":0.99658203,"speaker":"A"},{"text":"be","start":1027750,"end":1027950,"confidence":0.99902344,"speaker":"A"},{"text":"is","start":1027950,"end":1028150,"confidence":0.93359375,"speaker":"A"},{"text":"that","start":1028150,"end":1028310,"confidence":0.99853516,"speaker":"A"},{"text":"you'd","start":1028310,"end":1028630,"confidence":0.96516925,"speaker":"A"},{"text":"have","start":1028630,"end":1028910,"confidence":1,"speaker":"A"},{"text":"some","start":1029710,"end":1029990,"confidence":0.9995117,"speaker":"A"},{"text":"sort","start":1029990,"end":1030230,"confidence":0.99609375,"speaker":"A"},{"text":"of","start":1030230,"end":1030390,"confidence":0.9975586,"speaker":"A"},{"text":"app","start":1030390,"end":1030670,"confidence":0.99902344,"speaker":"A"},{"text":"that","start":1030670,"end":1030950,"confidence":0.9995117,"speaker":"A"},{"text":"would","start":1030950,"end":1031150,"confidence":0.9970703,"speaker":"A"},{"text":"use","start":1031150,"end":1031470,"confidence":0.99902344,"speaker":"A"},{"text":"central","start":1031550,"end":1031950,"confidence":0.9995117,"speaker":"A"},{"text":"repository","start":1031950,"end":1032790,"confidence":0.99694824,"speaker":"A"},{"text":"of","start":1032790,"end":1032990,"confidence":0.99853516,"speaker":"A"},{"text":"data","start":1032990,"end":1033310,"confidence":0.99902344,"speaker":"A"},{"text":"that","start":1035470,"end":1035790,"confidence":0.99902344,"speaker":"A"},{"text":"it","start":1035790,"end":1035950,"confidence":0.63134766,"speaker":"A"},{"text":"can","start":1035950,"end":1036070,"confidence":0.9980469,"speaker":"A"},{"text":"pull","start":1036070,"end":1036390,"confidence":0.99975586,"speaker":"A"},{"text":"information","start":1036390,"end":1036750,"confidence":1,"speaker":"A"},{"text":"from.","start":1036990,"end":1037390,"confidence":0.9995117,"speaker":"A"},{"text":"And","start":1037790,"end":1038110,"confidence":0.91259766,"speaker":"A"},{"text":"I'm","start":1038110,"end":1038390,"confidence":0.99104816,"speaker":"A"},{"text":"looking","start":1038390,"end":1038550,"confidence":0.9902344,"speaker":"A"},{"text":"at","start":1038550,"end":1038710,"confidence":0.99902344,"speaker":"A"},{"text":"both","start":1038710,"end":1038870,"confidence":1,"speaker":"A"},{"text":"of","start":1038870,"end":1039030,"confidence":0.9995117,"speaker":"A"},{"text":"these","start":1039030,"end":1039310,"confidence":0.9995117,"speaker":"A"},{"text":"with","start":1039310,"end":1039710,"confidence":0.99902344,"speaker":"A"},{"text":"Bushel","start":1039950,"end":1040590,"confidence":0.90722656,"speaker":"A"},{"text":"and","start":1040590,"end":1040790,"confidence":0.9975586,"speaker":"A"},{"text":"then","start":1040790,"end":1040950,"confidence":0.9584961,"speaker":"A"},{"text":"an","start":1040950,"end":1041190,"confidence":0.98291016,"speaker":"A"},{"text":"RSS","start":1041190,"end":1041670,"confidence":0.9987793,"speaker":"A"},{"text":"reader","start":1041670,"end":1042070,"confidence":0.9975586,"speaker":"A"},{"text":"I'm","start":1042070,"end":1042270,"confidence":0.93929034,"speaker":"A"},{"text":"building","start":1042270,"end":1042430,"confidence":0.9995117,"speaker":"A"},{"text":"called","start":1042430,"end":1042630,"confidence":0.9584961,"speaker":"A"},{"text":"Celestra","start":1042630,"end":1043310,"confidence":0.9358724,"speaker":"A"},{"text":"with","start":1044190,"end":1044510,"confidence":0.98535156,"speaker":"A"},{"text":"Bushel.","start":1044510,"end":1045150,"confidence":0.9350586,"speaker":"A"},{"text":"The.","start":1046199,"end":1046439,"confidence":0.84375,"speaker":"A"},{"text":"The","start":1046679,"end":1046959,"confidence":0.9980469,"speaker":"A"},{"text":"way","start":1046959,"end":1047119,"confidence":1,"speaker":"A"},{"text":"it's","start":1047119,"end":1047319,"confidence":0.9996745,"speaker":"A"},{"text":"built","start":1047319,"end":1047559,"confidence":0.8929036,"speaker":"A"},{"text":"right","start":1047559,"end":1047759,"confidence":0.9995117,"speaker":"A"},{"text":"now","start":1047759,"end":1047959,"confidence":0.9995117,"speaker":"A"},{"text":"is","start":1047959,"end":1048199,"confidence":0.9667969,"speaker":"A"},{"text":"I","start":1048199,"end":1048359,"confidence":0.99902344,"speaker":"A"},{"text":"have","start":1048359,"end":1048479,"confidence":0.9975586,"speaker":"A"},{"text":"this","start":1048479,"end":1048679,"confidence":0.9995117,"speaker":"A"},{"text":"concept","start":1048679,"end":1049079,"confidence":0.9786784,"speaker":"A"},{"text":"of","start":1049079,"end":1049319,"confidence":0.9995117,"speaker":"A"},{"text":"hubs","start":1049319,"end":1049719,"confidence":0.9838867,"speaker":"A"},{"text":"and","start":1050679,"end":1051079,"confidence":0.96240234,"speaker":"A"},{"text":"you","start":1051159,"end":1051439,"confidence":1,"speaker":"A"},{"text":"can","start":1051439,"end":1051599,"confidence":0.99902344,"speaker":"A"},{"text":"plug","start":1051599,"end":1051799,"confidence":1,"speaker":"A"},{"text":"in","start":1051799,"end":1051919,"confidence":0.9951172,"speaker":"A"},{"text":"a","start":1051919,"end":1052079,"confidence":0.99072266,"speaker":"A"},{"text":"URL","start":1052079,"end":1052639,"confidence":0.9992676,"speaker":"A"},{"text":"and","start":1052639,"end":1052839,"confidence":0.9628906,"speaker":"A"},{"text":"that","start":1052839,"end":1052959,"confidence":0.99902344,"speaker":"A"},{"text":"URL","start":1052959,"end":1053439,"confidence":0.9367676,"speaker":"A"},{"text":"would","start":1053439,"end":1053719,"confidence":0.99658203,"speaker":"A"},{"text":"provide","start":1053719,"end":1054039,"confidence":1,"speaker":"A"},{"text":"or","start":1054039,"end":1054399,"confidence":0.99902344,"speaker":"A"},{"text":"some","start":1054399,"end":1054679,"confidence":0.97216797,"speaker":"A"},{"text":"sort","start":1054679,"end":1054919,"confidence":0.9941406,"speaker":"A"},{"text":"of","start":1054919,"end":1055079,"confidence":0.99902344,"speaker":"A"},{"text":"service.","start":1055079,"end":1055399,"confidence":0.99902344,"speaker":"A"},{"text":"That","start":1055959,"end":1056359,"confidence":0.9980469,"speaker":"A"},{"text":"service","start":1056599,"end":1056999,"confidence":0.9980469,"speaker":"A"},{"text":"would","start":1056999,"end":1057279,"confidence":0.9941406,"speaker":"A"},{"text":"then","start":1057279,"end":1057479,"confidence":0.9916992,"speaker":"A"},{"text":"provide","start":1057479,"end":1057799,"confidence":1,"speaker":"A"},{"text":"the","start":1058359,"end":1058639,"confidence":0.9995117,"speaker":"A"},{"text":"Entire","start":1058639,"end":1058999,"confidence":0.99975586,"speaker":"A"},{"text":"List","start":1058999,"end":1059279,"confidence":0.9995117,"speaker":"A"},{"text":"of","start":1059279,"end":1059639,"confidence":0.99853516,"speaker":"A"},{"text":"macOS","start":1059719,"end":1060439,"confidence":0.76636,"speaker":"A"},{"text":"restore","start":1060439,"end":1060839,"confidence":0.98168945,"speaker":"A"},{"text":"images","start":1060839,"end":1061278,"confidence":0.9987793,"speaker":"A"},{"text":"that","start":1061278,"end":1061479,"confidence":0.9995117,"speaker":"A"},{"text":"are","start":1061479,"end":1061638,"confidence":0.9995117,"speaker":"A"},{"text":"available.","start":1061638,"end":1061959,"confidence":0.9995117,"speaker":"A"},{"text":"But","start":1064119,"end":1064399,"confidence":0.9941406,"speaker":"A"},{"text":"then","start":1064399,"end":1064559,"confidence":0.9995117,"speaker":"A"},{"text":"I","start":1064559,"end":1064719,"confidence":0.9995117,"speaker":"A"},{"text":"realized","start":1064719,"end":1065079,"confidence":0.9863281,"speaker":"A"},{"text":"like","start":1065079,"end":1065319,"confidence":0.90283203,"speaker":"A"},{"text":"really","start":1065319,"end":1065559,"confidence":0.9970703,"speaker":"A"},{"text":"there's","start":1065559,"end":1065839,"confidence":0.9889323,"speaker":"A"},{"text":"only","start":1065839,"end":1065999,"confidence":0.9995117,"speaker":"A"},{"text":"one","start":1065999,"end":1066199,"confidence":0.9995117,"speaker":"A"},{"text":"location","start":1066199,"end":1066679,"confidence":1,"speaker":"A"},{"text":"for","start":1066679,"end":1066919,"confidence":0.9995117,"speaker":"A"},{"text":"those","start":1066919,"end":1067239,"confidence":0.9995117,"speaker":"A"},{"text":"and","start":1067319,"end":1067719,"confidence":0.98876953,"speaker":"A"},{"text":"each","start":1067719,"end":1068079,"confidence":0.9824219,"speaker":"A"},{"text":"service","start":1068079,"end":1068399,"confidence":0.9951172,"speaker":"A"},{"text":"is","start":1068399,"end":1068639,"confidence":0.99853516,"speaker":"A"},{"text":"just","start":1068639,"end":1068799,"confidence":0.99609375,"speaker":"A"},{"text":"going","start":1068799,"end":1068919,"confidence":0.8798828,"speaker":"A"},{"text":"to","start":1068919,"end":1068999,"confidence":0.99902344,"speaker":"A"},{"text":"be","start":1068999,"end":1069079,"confidence":0.99853516,"speaker":"A"},{"text":"using","start":1069079,"end":1069319,"confidence":0.9995117,"speaker":"A"},{"text":"the","start":1069319,"end":1069559,"confidence":0.9995117,"speaker":"A"},{"text":"same","start":1069559,"end":1069719,"confidence":0.9995117,"speaker":"A"},{"text":"URLs","start":1069719,"end":1070359,"confidence":0.92261,"speaker":"A"},{"text":"anyway.","start":1070359,"end":1070839,"confidence":0.99731445,"speaker":"A"},{"text":"So","start":1071970,"end":1072050,"confidence":0.92822266,"speaker":"A"},{"text":"if","start":1072050,"end":1072170,"confidence":0.9995117,"speaker":"A"},{"text":"I","start":1072170,"end":1072330,"confidence":0.9995117,"speaker":"A"},{"text":"had","start":1072330,"end":1072570,"confidence":0.9975586,"speaker":"A"},{"text":"one","start":1072570,"end":1072850,"confidence":0.9995117,"speaker":"A"},{"text":"central","start":1072850,"end":1073170,"confidence":1,"speaker":"A"},{"text":"repository","start":1073250,"end":1074050,"confidence":0.9127197,"speaker":"A"},{"text":"or","start":1074050,"end":1074250,"confidence":0.99853516,"speaker":"A"},{"text":"one","start":1074250,"end":1074450,"confidence":0.9970703,"speaker":"A"},{"text":"central","start":1074450,"end":1074770,"confidence":1,"speaker":"A"},{"text":"database","start":1074770,"end":1075490,"confidence":1,"speaker":"A"},{"text":"because","start":1076850,"end":1077170,"confidence":0.99365234,"speaker":"A"},{"text":"they","start":1077170,"end":1077370,"confidence":0.9975586,"speaker":"A"},{"text":"all","start":1077370,"end":1077530,"confidence":0.99902344,"speaker":"A"},{"text":"pull","start":1077530,"end":1077770,"confidence":0.99975586,"speaker":"A"},{"text":"from","start":1077770,"end":1077970,"confidence":0.9995117,"speaker":"A"},{"text":"Apple,","start":1077970,"end":1078450,"confidence":0.9995117,"speaker":"A"},{"text":"I","start":1078690,"end":1079010,"confidence":0.99853516,"speaker":"A"},{"text":"can","start":1079010,"end":1079210,"confidence":0.99365234,"speaker":"A"},{"text":"then","start":1079210,"end":1079490,"confidence":0.98828125,"speaker":"A"},{"text":"parse","start":1079650,"end":1080250,"confidence":0.8129883,"speaker":"A"},{"text":"the","start":1080250,"end":1080490,"confidence":0.9995117,"speaker":"A"},{"text":"web","start":1080490,"end":1080850,"confidence":0.99975586,"speaker":"A"},{"text":"for","start":1081090,"end":1081410,"confidence":0.59033203,"speaker":"A"},{"text":"those","start":1081410,"end":1081690,"confidence":0.99902344,"speaker":"A"},{"text":"restore","start":1081690,"end":1082210,"confidence":0.98779297,"speaker":"A"},{"text":"images","start":1082210,"end":1082690,"confidence":0.99780273,"speaker":"A"},{"text":"and","start":1082690,"end":1082930,"confidence":0.99072266,"speaker":"A"},{"text":"then","start":1082930,"end":1083090,"confidence":0.99658203,"speaker":"A"},{"text":"store","start":1083090,"end":1083370,"confidence":0.9736328,"speaker":"A"},{"text":"them","start":1083370,"end":1083530,"confidence":0.9238281,"speaker":"A"},{"text":"in","start":1083530,"end":1083650,"confidence":0.98779297,"speaker":"A"},{"text":"CloudKit","start":1083650,"end":1084210,"confidence":0.94812,"speaker":"A"},{"text":"and","start":1084210,"end":1084370,"confidence":0.8354492,"speaker":"A"},{"text":"then","start":1084370,"end":1084530,"confidence":0.9873047,"speaker":"A"},{"text":"that","start":1084530,"end":1084770,"confidence":0.9980469,"speaker":"A"},{"text":"way","start":1084770,"end":1085090,"confidence":0.99853516,"speaker":"A"},{"text":"Bushel","start":1085410,"end":1086010,"confidence":0.8808594,"speaker":"A"},{"text":"can","start":1086010,"end":1086170,"confidence":0.9501953,"speaker":"A"},{"text":"then","start":1086170,"end":1086450,"confidence":0.95751953,"speaker":"A"},{"text":"pull","start":1087570,"end":1087930,"confidence":0.9995117,"speaker":"A"},{"text":"those","start":1087930,"end":1088210,"confidence":0.9975586,"speaker":"A"},{"text":"from","start":1088210,"end":1088530,"confidence":1,"speaker":"A"},{"text":"one","start":1088530,"end":1088770,"confidence":0.9995117,"speaker":"A"},{"text":"single","start":1088770,"end":1089090,"confidence":1,"speaker":"A"},{"text":"repository.","start":1089090,"end":1089970,"confidence":0.9998779,"speaker":"A"},{"text":"And","start":1090210,"end":1090490,"confidence":0.86572266,"speaker":"A"},{"text":"all","start":1090490,"end":1090650,"confidence":0.99853516,"speaker":"A"},{"text":"I","start":1090650,"end":1090770,"confidence":0.98291016,"speaker":"A"},{"text":"would","start":1090770,"end":1090930,"confidence":0.98583984,"speaker":"A"},{"text":"have","start":1090930,"end":1091090,"confidence":1,"speaker":"A"},{"text":"to","start":1091090,"end":1091210,"confidence":0.99902344,"speaker":"A"},{"text":"do,","start":1091210,"end":1091450,"confidence":0.9995117,"speaker":"A"},{"text":"and","start":1091450,"end":1091770,"confidence":0.64404297,"speaker":"A"},{"text":"what","start":1091770,"end":1092010,"confidence":0.9995117,"speaker":"A"},{"text":"I'm","start":1092010,"end":1092210,"confidence":0.99934894,"speaker":"A"},{"text":"doing","start":1092210,"end":1092410,"confidence":1,"speaker":"A"},{"text":"now","start":1092410,"end":1092690,"confidence":0.99853516,"speaker":"A"},{"text":"is","start":1092690,"end":1092930,"confidence":0.99902344,"speaker":"A"},{"text":"running","start":1092930,"end":1093370,"confidence":0.99121094,"speaker":"A"},{"text":"basically","start":1093370,"end":1093850,"confidence":0.998291,"speaker":"A"},{"text":"a","start":1093850,"end":1094090,"confidence":0.9951172,"speaker":"A"},{"text":"GitHub","start":1094090,"end":1094490,"confidence":0.9991862,"speaker":"A"},{"text":"action","start":1094490,"end":1094690,"confidence":1,"speaker":"A"},{"text":"or","start":1094690,"end":1094850,"confidence":0.98828125,"speaker":"A"},{"text":"you","start":1094850,"end":1094930,"confidence":0.91503906,"speaker":"A"},{"text":"could","start":1094930,"end":1095050,"confidence":0.8876953,"speaker":"A"},{"text":"do","start":1095050,"end":1095210,"confidence":0.99853516,"speaker":"A"},{"text":"like","start":1095210,"end":1095370,"confidence":0.8642578,"speaker":"A"},{"text":"a","start":1095370,"end":1095490,"confidence":0.9868164,"speaker":"A"},{"text":"Cron","start":1095490,"end":1095770,"confidence":0.97875977,"speaker":"A"},{"text":"job","start":1095770,"end":1096050,"confidence":1,"speaker":"A"},{"text":"where","start":1096450,"end":1096850,"confidence":0.9995117,"speaker":"A"},{"text":"it","start":1096850,"end":1097130,"confidence":0.99560547,"speaker":"A"},{"text":"would","start":1097130,"end":1097290,"confidence":1,"speaker":"A"},{"text":"run","start":1097290,"end":1097450,"confidence":0.9995117,"speaker":"A"},{"text":"on","start":1097450,"end":1097610,"confidence":0.9824219,"speaker":"A"},{"text":"Ubuntu,","start":1097610,"end":1098090,"confidence":0.8498047,"speaker":"A"},{"text":"wouldn't","start":1098090,"end":1098370,"confidence":0.9715576,"speaker":"A"},{"text":"even","start":1098370,"end":1098490,"confidence":0.99853516,"speaker":"A"},{"text":"need","start":1098490,"end":1098650,"confidence":0.99853516,"speaker":"A"},{"text":"a","start":1098650,"end":1098810,"confidence":0.99853516,"speaker":"A"},{"text":"Mac","start":1098810,"end":1099090,"confidence":0.9992676,"speaker":"A"},{"text":"and","start":1099090,"end":1099290,"confidence":0.96240234,"speaker":"A"},{"text":"it","start":1099290,"end":1099450,"confidence":0.99853516,"speaker":"A"},{"text":"would","start":1099450,"end":1099730,"confidence":0.9995117,"speaker":"A"},{"text":"download","start":1099890,"end":1100490,"confidence":1,"speaker":"A"},{"text":"and","start":1100490,"end":1100730,"confidence":0.59228516,"speaker":"A"},{"text":"scrape","start":1100730,"end":1101130,"confidence":0.8902588,"speaker":"A"},{"text":"the","start":1101130,"end":1101290,"confidence":0.9995117,"speaker":"A"},{"text":"web","start":1101290,"end":1101530,"confidence":0.9995117,"speaker":"A"},{"text":"for","start":1101530,"end":1101770,"confidence":0.9970703,"speaker":"A"},{"text":"restore","start":1101770,"end":1102250,"confidence":0.9777832,"speaker":"A"},{"text":"images","start":1102250,"end":1102650,"confidence":0.99731445,"speaker":"A"},{"text":"and","start":1102650,"end":1103000,"confidence":0.52197266,"speaker":"A"},{"text":"storm","start":1103070,"end":1103350,"confidence":0.92749023,"speaker":"A"},{"text":"in","start":1103350,"end":1103470,"confidence":0.9951172,"speaker":"A"},{"text":"the","start":1103470,"end":1103590,"confidence":0.99902344,"speaker":"A"},{"text":"public","start":1103590,"end":1103790,"confidence":1,"speaker":"A"},{"text":"database.","start":1103790,"end":1104430,"confidence":0.99820966,"speaker":"A"},{"text":"It's","start":1106350,"end":1106710,"confidence":0.9967448,"speaker":"A"},{"text":"the","start":1106710,"end":1106830,"confidence":0.9995117,"speaker":"A"},{"text":"same","start":1106830,"end":1106950,"confidence":1,"speaker":"A"},{"text":"idea","start":1106950,"end":1107230,"confidence":0.99902344,"speaker":"A"},{"text":"with","start":1107230,"end":1107350,"confidence":0.98779297,"speaker":"A"},{"text":"Celestra.","start":1107350,"end":1107910,"confidence":0.9313151,"speaker":"A"},{"text":"It's","start":1107910,"end":1108110,"confidence":0.99283856,"speaker":"A"},{"text":"an","start":1108110,"end":1108190,"confidence":0.73876953,"speaker":"A"},{"text":"RSS","start":1108190,"end":1108630,"confidence":0.9946289,"speaker":"A"},{"text":"reader.","start":1108630,"end":1109110,"confidence":0.99902344,"speaker":"A"},{"text":"What","start":1109110,"end":1109270,"confidence":0.9995117,"speaker":"A"},{"text":"if","start":1109270,"end":1109430,"confidence":0.9995117,"speaker":"A"},{"text":"I","start":1109430,"end":1109630,"confidence":0.9995117,"speaker":"A"},{"text":"took","start":1109630,"end":1109870,"confidence":0.99902344,"speaker":"A"},{"text":"those","start":1109870,"end":1110070,"confidence":0.9946289,"speaker":"A"},{"text":"RSS","start":1110070,"end":1110590,"confidence":0.98535156,"speaker":"A"},{"text":"RSS","start":1112750,"end":1113310,"confidence":0.94921875,"speaker":"A"},{"text":"files","start":1113310,"end":1113670,"confidence":0.95703125,"speaker":"A"},{"text":"in","start":1113670,"end":1113830,"confidence":0.99365234,"speaker":"A"},{"text":"the","start":1113830,"end":1113950,"confidence":1,"speaker":"A"},{"text":"web","start":1113950,"end":1114150,"confidence":0.99975586,"speaker":"A"},{"text":"and","start":1114150,"end":1114350,"confidence":0.8354492,"speaker":"A"},{"text":"just","start":1114350,"end":1114630,"confidence":0.99853516,"speaker":"A"},{"text":"scrape","start":1114630,"end":1115110,"confidence":0.8651123,"speaker":"A"},{"text":"them","start":1115110,"end":1115270,"confidence":0.9995117,"speaker":"A"},{"text":"and","start":1115270,"end":1115430,"confidence":0.99902344,"speaker":"A"},{"text":"then","start":1115430,"end":1115630,"confidence":0.9970703,"speaker":"A"},{"text":"store","start":1115630,"end":1115950,"confidence":0.97753906,"speaker":"A"},{"text":"them","start":1115950,"end":1116070,"confidence":0.99902344,"speaker":"A"},{"text":"in","start":1116070,"end":1116190,"confidence":0.99902344,"speaker":"A"},{"text":"a","start":1116190,"end":1116270,"confidence":0.99902344,"speaker":"A"},{"text":"CloudKit","start":1116270,"end":1116830,"confidence":0.9890137,"speaker":"A"},{"text":"database","start":1116830,"end":1117470,"confidence":0.9996745,"speaker":"A"},{"text":"in","start":1118110,"end":1118430,"confidence":0.8745117,"speaker":"A"},{"text":"a","start":1118430,"end":1118590,"confidence":0.99902344,"speaker":"A"},{"text":"public","start":1118590,"end":1118750,"confidence":1,"speaker":"A"},{"text":"database","start":1118750,"end":1119390,"confidence":0.9998372,"speaker":"A"},{"text":"and","start":1119390,"end":1119550,"confidence":0.99316406,"speaker":"A"},{"text":"then","start":1119550,"end":1119710,"confidence":0.9741211,"speaker":"A"},{"text":"that","start":1119710,"end":1119910,"confidence":0.9995117,"speaker":"A"},{"text":"way","start":1119910,"end":1120110,"confidence":1,"speaker":"A"},{"text":"people","start":1120110,"end":1120390,"confidence":1,"speaker":"A"},{"text":"can","start":1120390,"end":1120750,"confidence":0.9995117,"speaker":"A"},{"text":"pull","start":1120750,"end":1121110,"confidence":1,"speaker":"A"},{"text":"that","start":1121110,"end":1121310,"confidence":0.99853516,"speaker":"A"},{"text":"up","start":1121310,"end":1121630,"confidence":0.99853516,"speaker":"A"},{"text":"all","start":1121630,"end":1121910,"confidence":0.9980469,"speaker":"A"},{"text":"through","start":1121910,"end":1122110,"confidence":1,"speaker":"A"},{"text":"CloudKit.","start":1122110,"end":1122910,"confidence":0.845459,"speaker":"A"},{"text":"So","start":1125150,"end":1125550,"confidence":0.9873047,"speaker":"A"},{"text":"the","start":1125630,"end":1125910,"confidence":0.99902344,"speaker":"A"},{"text":"idea","start":1125910,"end":1126270,"confidence":1,"speaker":"A"},{"text":"today","start":1126270,"end":1126550,"confidence":0.9995117,"speaker":"A"},{"text":"is","start":1126550,"end":1126790,"confidence":0.9980469,"speaker":"A"},{"text":"we're","start":1126790,"end":1127030,"confidence":0.9991862,"speaker":"A"},{"text":"going","start":1127030,"end":1127150,"confidence":0.88671875,"speaker":"A"},{"text":"to","start":1127150,"end":1127230,"confidence":1,"speaker":"A"},{"text":"talk","start":1127230,"end":1127390,"confidence":0.9995117,"speaker":"A"},{"text":"about","start":1127390,"end":1127710,"confidence":0.9975586,"speaker":"A"},{"text":"how","start":1128030,"end":1128350,"confidence":0.99365234,"speaker":"A"},{"text":"to","start":1128350,"end":1128550,"confidence":0.9707031,"speaker":"A"},{"text":"set","start":1128550,"end":1128750,"confidence":0.99853516,"speaker":"A"},{"text":"something,","start":1128750,"end":1129070,"confidence":0.95947266,"speaker":"A"},{"text":"how","start":1129070,"end":1129430,"confidence":0.9814453,"speaker":"A"},{"text":"I","start":1129430,"end":1129710,"confidence":0.99560547,"speaker":"A"},{"text":"set","start":1129710,"end":1129990,"confidence":0.99658203,"speaker":"A"},{"text":"something","start":1129990,"end":1130310,"confidence":1,"speaker":"A"},{"text":"like","start":1130310,"end":1130550,"confidence":0.99902344,"speaker":"A"},{"text":"this","start":1130550,"end":1130750,"confidence":0.9995117,"speaker":"A"},{"text":"up","start":1130750,"end":1131070,"confidence":0.99560547,"speaker":"A"},{"text":"and","start":1131860,"end":1132100,"confidence":0.9321289,"speaker":"A"},{"text":"how","start":1132100,"end":1132380,"confidence":0.9995117,"speaker":"A"},{"text":"you","start":1132380,"end":1132540,"confidence":0.99902344,"speaker":"A"},{"text":"could","start":1132540,"end":1132740,"confidence":0.99560547,"speaker":"A"},{"text":"use","start":1132740,"end":1133060,"confidence":0.9277344,"speaker":"A"},{"text":"use","start":1133300,"end":1133580,"confidence":1,"speaker":"A"},{"text":"my","start":1133580,"end":1133780,"confidence":0.99121094,"speaker":"A"},{"text":"library","start":1133780,"end":1134260,"confidence":0.9998372,"speaker":"A"},{"text":"to","start":1134260,"end":1134460,"confidence":0.9975586,"speaker":"A"},{"text":"then","start":1134460,"end":1134620,"confidence":0.9980469,"speaker":"A"},{"text":"go","start":1134620,"end":1134780,"confidence":0.99902344,"speaker":"A"},{"text":"ahead","start":1134780,"end":1134980,"confidence":0.9995117,"speaker":"A"},{"text":"and","start":1134980,"end":1135220,"confidence":0.53125,"speaker":"A"},{"text":"do","start":1135220,"end":1135420,"confidence":1,"speaker":"A"},{"text":"this","start":1135420,"end":1135620,"confidence":1,"speaker":"A"},{"text":"yourself","start":1135620,"end":1136060,"confidence":0.99975586,"speaker":"A"},{"text":"for","start":1136060,"end":1136340,"confidence":0.9995117,"speaker":"A"},{"text":"any","start":1136340,"end":1136660,"confidence":0.9995117,"speaker":"A"},{"text":"sort","start":1136660,"end":1136980,"confidence":0.9975586,"speaker":"A"},{"text":"of","start":1136980,"end":1137100,"confidence":0.9995117,"speaker":"A"},{"text":"work","start":1137100,"end":1137340,"confidence":0.9995117,"speaker":"A"},{"text":"that","start":1137340,"end":1137580,"confidence":0.99853516,"speaker":"A"},{"text":"you're","start":1137580,"end":1137780,"confidence":0.99886066,"speaker":"A"},{"text":"going","start":1137780,"end":1137860,"confidence":0.7861328,"speaker":"A"},{"text":"to","start":1137860,"end":1137940,"confidence":0.99853516,"speaker":"A"},{"text":"do","start":1137940,"end":1138060,"confidence":0.99902344,"speaker":"A"},{"text":"that","start":1138060,"end":1138260,"confidence":0.9140625,"speaker":"A"},{"text":"where","start":1138260,"end":1138460,"confidence":0.9970703,"speaker":"A"},{"text":"you","start":1138460,"end":1138580,"confidence":1,"speaker":"A"},{"text":"want","start":1138580,"end":1138700,"confidence":0.9140625,"speaker":"A"},{"text":"to","start":1138700,"end":1138860,"confidence":0.9941406,"speaker":"A"},{"text":"use","start":1138860,"end":1139100,"confidence":0.99609375,"speaker":"A"},{"text":"either","start":1139100,"end":1139420,"confidence":0.99975586,"speaker":"A"},{"text":"a","start":1139420,"end":1139580,"confidence":0.9238281,"speaker":"A"},{"text":"public","start":1139580,"end":1139780,"confidence":1,"speaker":"A"},{"text":"or","start":1139780,"end":1140020,"confidence":1,"speaker":"A"},{"text":"private","start":1140020,"end":1140300,"confidence":1,"speaker":"A"},{"text":"database","start":1140300,"end":1140980,"confidence":0.9995117,"speaker":"A"},{"text":"in","start":1141220,"end":1141500,"confidence":0.7890625,"speaker":"A"},{"text":"CloudKit.","start":1141500,"end":1142180,"confidence":0.99560547,"speaker":"A"},{"text":"So","start":1143300,"end":1143540,"confidence":0.9873047,"speaker":"A"},{"text":"this","start":1143540,"end":1143660,"confidence":0.9995117,"speaker":"A"},{"text":"is","start":1143660,"end":1143820,"confidence":1,"speaker":"A"},{"text":"where","start":1143820,"end":1143980,"confidence":0.9995117,"speaker":"A"},{"text":"I","start":1143980,"end":1144140,"confidence":0.97509766,"speaker":"A"},{"text":"introduce","start":1144140,"end":1144580,"confidence":0.96435547,"speaker":"A"},{"text":"myself.","start":1144580,"end":1145060,"confidence":0.99487305,"speaker":"A"},{"text":"So","start":1145940,"end":1146180,"confidence":0.9741211,"speaker":"A"},{"text":"I'm","start":1146180,"end":1146340,"confidence":0.99690753,"speaker":"A"},{"text":"going","start":1146340,"end":1146420,"confidence":0.9428711,"speaker":"A"},{"text":"to","start":1146420,"end":1146500,"confidence":0.99853516,"speaker":"A"},{"text":"talk","start":1146500,"end":1146660,"confidence":0.9995117,"speaker":"A"},{"text":"today","start":1146660,"end":1146860,"confidence":0.99121094,"speaker":"A"},{"text":"about","start":1146860,"end":1147020,"confidence":1,"speaker":"A"},{"text":"building","start":1147020,"end":1147299,"confidence":0.9995117,"speaker":"A"},{"text":"Miskit,","start":1147299,"end":1148020,"confidence":0.82421875,"speaker":"A"},{"text":"which","start":1148260,"end":1148540,"confidence":0.9995117,"speaker":"A"},{"text":"is","start":1148540,"end":1148700,"confidence":0.99072266,"speaker":"A"},{"text":"my","start":1148700,"end":1148860,"confidence":0.9995117,"speaker":"A"},{"text":"library","start":1148860,"end":1149300,"confidence":1,"speaker":"A"},{"text":"I","start":1149300,"end":1149500,"confidence":0.99853516,"speaker":"A"},{"text":"built","start":1149500,"end":1149860,"confidence":0.96761066,"speaker":"A"},{"text":"for","start":1150340,"end":1150700,"confidence":0.9921875,"speaker":"A"},{"text":"doing","start":1150700,"end":1151060,"confidence":0.9995117,"speaker":"A"},{"text":"CloudKit","start":1151460,"end":1152100,"confidence":0.99609375,"speaker":"A"},{"text":"stuff","start":1152100,"end":1152580,"confidence":0.99886066,"speaker":"A"},{"text":"on","start":1152740,"end":1153020,"confidence":0.94628906,"speaker":"A"},{"text":"the","start":1153020,"end":1153180,"confidence":0.9995117,"speaker":"A"},{"text":"server","start":1153180,"end":1153540,"confidence":1,"speaker":"A"},{"text":"or","start":1153540,"end":1153740,"confidence":0.9951172,"speaker":"A"},{"text":"essentially","start":1153740,"end":1154180,"confidence":0.9970703,"speaker":"A"},{"text":"off","start":1154180,"end":1154420,"confidence":0.8652344,"speaker":"A"},{"text":"of,","start":1154420,"end":1154740,"confidence":0.9970703,"speaker":"A"},{"text":"not","start":1155380,"end":1155660,"confidence":0.99853516,"speaker":"A"},{"text":"off","start":1155660,"end":1155860,"confidence":0.9995117,"speaker":"A"},{"text":"of","start":1155860,"end":1156100,"confidence":0.9970703,"speaker":"A"},{"text":"Apple","start":1156100,"end":1156500,"confidence":0.99975586,"speaker":"A"},{"text":"platforms.","start":1156500,"end":1157140,"confidence":0.9978841,"speaker":"A"},{"text":"Evan,","start":1159770,"end":1160050,"confidence":0.9189453,"speaker":"A"},{"text":"do","start":1160050,"end":1160170,"confidence":0.9980469,"speaker":"A"},{"text":"you","start":1160170,"end":1160250,"confidence":0.9873047,"speaker":"A"},{"text":"have","start":1160250,"end":1160330,"confidence":0.9995117,"speaker":"A"},{"text":"any","start":1160330,"end":1160450,"confidence":0.99902344,"speaker":"A"},{"text":"questions","start":1160450,"end":1160850,"confidence":0.99975586,"speaker":"A"},{"text":"before","start":1160850,"end":1161010,"confidence":1,"speaker":"A"},{"text":"I","start":1161010,"end":1161170,"confidence":0.99853516,"speaker":"A"},{"text":"keep","start":1161170,"end":1161330,"confidence":0.99902344,"speaker":"A"},{"text":"going?","start":1161330,"end":1161610,"confidence":0.99902344,"speaker":"A"},{"text":"No,","start":1162730,"end":1163130,"confidence":0.9770508,"speaker":"B"},{"text":"it's","start":1163370,"end":1163730,"confidence":0.9757487,"speaker":"B"},{"text":"good.","start":1163730,"end":1163970,"confidence":0.6723633,"speaker":"B"},{"text":"Good","start":1163970,"end":1164250,"confidence":1,"speaker":"B"},{"text":"topic","start":1164250,"end":1164610,"confidence":0.9953613,"speaker":"B"},{"text":"though.","start":1164610,"end":1164890,"confidence":0.99072266,"speaker":"B"},{"text":"So","start":1166810,"end":1167090,"confidence":0.9042969,"speaker":"A"},{"text":"like","start":1167090,"end":1167250,"confidence":0.9951172,"speaker":"A"},{"text":"I","start":1167250,"end":1167410,"confidence":1,"speaker":"A"},{"text":"said,","start":1167410,"end":1167610,"confidence":1,"speaker":"A"},{"text":"we","start":1167610,"end":1167810,"confidence":1,"speaker":"A"},{"text":"have","start":1167810,"end":1167970,"confidence":1,"speaker":"A"},{"text":"CloudKit","start":1167970,"end":1168570,"confidence":0.86804,"speaker":"A"},{"text":"Web","start":1168570,"end":1168810,"confidence":0.99853516,"speaker":"A"},{"text":"Services","start":1168810,"end":1169050,"confidence":0.99853516,"speaker":"A"},{"text":"and","start":1170170,"end":1170530,"confidence":0.8461914,"speaker":"A"},{"text":"CloudKit","start":1170530,"end":1171090,"confidence":0.9489746,"speaker":"A"},{"text":"Web","start":1171090,"end":1171330,"confidence":0.9975586,"speaker":"A"},{"text":"Services.","start":1171330,"end":1171610,"confidence":0.99902344,"speaker":"A"},{"text":"We","start":1172330,"end":1172730,"confidence":0.53759766,"speaker":"A"},{"text":"provide","start":1172730,"end":1173090,"confidence":1,"speaker":"A"},{"text":"a","start":1173090,"end":1173329,"confidence":0.96240234,"speaker":"A"},{"text":"lot","start":1173329,"end":1173489,"confidence":1,"speaker":"A"},{"text":"of","start":1173489,"end":1173610,"confidence":0.99853516,"speaker":"A"},{"text":"documentation.","start":1173610,"end":1174210,"confidence":0.99990237,"speaker":"A"},{"text":"We","start":1174210,"end":1174450,"confidence":0.99902344,"speaker":"A"},{"text":"talked","start":1174450,"end":1174650,"confidence":0.9987793,"speaker":"A"},{"text":"about","start":1174650,"end":1174770,"confidence":0.99902344,"speaker":"A"},{"text":"CloudKit","start":1174770,"end":1175330,"confidence":0.9980469,"speaker":"A"},{"text":"JS","start":1175330,"end":1175770,"confidence":0.7067871,"speaker":"A"},{"text":"and","start":1175850,"end":1176170,"confidence":0.9921875,"speaker":"A"},{"text":"the","start":1176170,"end":1176370,"confidence":0.9819336,"speaker":"A"},{"text":"instructions","start":1176370,"end":1176890,"confidence":0.9773763,"speaker":"A"},{"text":"on","start":1176890,"end":1177090,"confidence":0.9995117,"speaker":"A"},{"text":"how","start":1177090,"end":1177290,"confidence":0.9995117,"speaker":"A"},{"text":"to","start":1177290,"end":1177530,"confidence":0.9995117,"speaker":"A"},{"text":"compose","start":1177530,"end":1177930,"confidence":0.99853516,"speaker":"A"},{"text":"a","start":1177930,"end":1178090,"confidence":0.9926758,"speaker":"A"},{"text":"web","start":1178090,"end":1178410,"confidence":0.9980469,"speaker":"A"},{"text":"service","start":1178650,"end":1179050,"confidence":0.9902344,"speaker":"A"},{"text":"request","start":1179050,"end":1179570,"confidence":0.99853516,"speaker":"A"},{"text":"which","start":1179570,"end":1179810,"confidence":0.99902344,"speaker":"A"},{"text":"has","start":1179810,"end":1180090,"confidence":0.9975586,"speaker":"A"},{"text":"everything","start":1180090,"end":1180450,"confidence":0.9995117,"speaker":"A"},{"text":"I","start":1180450,"end":1180730,"confidence":0.9980469,"speaker":"A"},{"text":"need","start":1180730,"end":1181050,"confidence":0.99853516,"speaker":"A"},{"text":"to","start":1181210,"end":1181490,"confidence":0.99853516,"speaker":"A"},{"text":"compose","start":1181490,"end":1181810,"confidence":0.99487305,"speaker":"A"},{"text":"one.","start":1181810,"end":1182050,"confidence":0.57421875,"speaker":"A"},{"text":"And","start":1182050,"end":1182370,"confidence":0.81640625,"speaker":"A"},{"text":"back","start":1182370,"end":1182610,"confidence":1,"speaker":"A"},{"text":"in","start":1182610,"end":1182810,"confidence":0.9995117,"speaker":"A"},{"text":"2020","start":1182810,"end":1183370,"confidence":0.9978,"speaker":"A"},{"text":"I","start":1183370,"end":1183610,"confidence":0.9995117,"speaker":"A"},{"text":"did","start":1183610,"end":1183730,"confidence":0.99902344,"speaker":"A"},{"text":"this","start":1183730,"end":1183890,"confidence":0.98535156,"speaker":"A"},{"text":"all","start":1183890,"end":1184090,"confidence":0.99316406,"speaker":"A"},{"text":"manually.","start":1184090,"end":1184570,"confidence":0.9992676,"speaker":"A"},{"text":"The","start":1186600,"end":1186760,"confidence":0.9946289,"speaker":"A"},{"text":"thing","start":1186760,"end":1187000,"confidence":0.9995117,"speaker":"A"},{"text":"is","start":1187000,"end":1187240,"confidence":0.99902344,"speaker":"A"},{"text":"at","start":1187240,"end":1187440,"confidence":0.99902344,"speaker":"A"},{"text":"this","start":1187440,"end":1187640,"confidence":0.9995117,"speaker":"A"},{"text":"point,","start":1187640,"end":1187960,"confidence":0.9995117,"speaker":"A"},{"text":"if","start":1188600,"end":1188880,"confidence":0.99902344,"speaker":"A"},{"text":"you","start":1188880,"end":1189040,"confidence":0.9995117,"speaker":"A"},{"text":"look","start":1189040,"end":1189200,"confidence":0.9995117,"speaker":"A"},{"text":"at","start":1189200,"end":1189440,"confidence":0.9814453,"speaker":"A"},{"text":"right","start":1189440,"end":1189720,"confidence":0.99902344,"speaker":"A"},{"text":"there,","start":1189720,"end":1190040,"confidence":0.99902344,"speaker":"A"},{"text":"actually","start":1191000,"end":1191320,"confidence":0.99316406,"speaker":"A"},{"text":"if","start":1191320,"end":1191480,"confidence":0.9995117,"speaker":"A"},{"text":"you","start":1191480,"end":1191560,"confidence":0.9995117,"speaker":"A"},{"text":"look","start":1191560,"end":1191680,"confidence":1,"speaker":"A"},{"text":"at","start":1191680,"end":1191800,"confidence":0.9995117,"speaker":"A"},{"text":"the","start":1191800,"end":1191920,"confidence":0.9995117,"speaker":"A"},{"text":"top,","start":1191920,"end":1192120,"confidence":0.9995117,"speaker":"A"},{"text":"you","start":1192120,"end":1192280,"confidence":0.9995117,"speaker":"A"},{"text":"can","start":1192280,"end":1192400,"confidence":1,"speaker":"A"},{"text":"see","start":1192400,"end":1192600,"confidence":0.9995117,"speaker":"A"},{"text":"it","start":1192600,"end":1192760,"confidence":0.98828125,"speaker":"A"},{"text":"hasn't","start":1192760,"end":1193080,"confidence":0.99768066,"speaker":"A"},{"text":"been","start":1193080,"end":1193200,"confidence":0.9995117,"speaker":"A"},{"text":"updated","start":1193200,"end":1193560,"confidence":0.99975586,"speaker":"A"},{"text":"in","start":1193560,"end":1193800,"confidence":0.96875,"speaker":"A"},{"text":"over","start":1193800,"end":1194120,"confidence":0.99902344,"speaker":"A"},{"text":"10","start":1194200,"end":1194480,"confidence":0.99951,"speaker":"A"},{"text":"years,","start":1194480,"end":1194760,"confidence":0.99902344,"speaker":"A"},{"text":"which","start":1196600,"end":1196880,"confidence":0.9975586,"speaker":"A"},{"text":"is","start":1196880,"end":1197160,"confidence":0.99853516,"speaker":"A"},{"text":"kind","start":1197160,"end":1197440,"confidence":0.88671875,"speaker":"A"},{"text":"of","start":1197440,"end":1197600,"confidence":0.9736328,"speaker":"A"},{"text":"crazy,","start":1197600,"end":1198120,"confidence":0.9996745,"speaker":"A"},{"text":"but","start":1198920,"end":1199200,"confidence":0.99609375,"speaker":"A"},{"text":"it","start":1199200,"end":1199360,"confidence":0.99902344,"speaker":"A"},{"text":"works.","start":1199360,"end":1199800,"confidence":0.99731445,"speaker":"A"},{"text":"And","start":1200999,"end":1201280,"confidence":0.7661133,"speaker":"A"},{"text":"then","start":1201280,"end":1201560,"confidence":0.9995117,"speaker":"A"},{"text":"we","start":1202040,"end":1202440,"confidence":0.9975586,"speaker":"A"},{"text":"got","start":1202840,"end":1203240,"confidence":0.96191406,"speaker":"A"},{"text":"introduced","start":1204200,"end":1204800,"confidence":0.9563802,"speaker":"A"},{"text":"to","start":1204800,"end":1204960,"confidence":0.9355469,"speaker":"A"},{"text":"something","start":1204960,"end":1205200,"confidence":0.9970703,"speaker":"A"},{"text":"back","start":1205200,"end":1205440,"confidence":0.9951172,"speaker":"A"},{"text":"in","start":1205440,"end":1205600,"confidence":0.9897461,"speaker":"A"},{"text":"WWDC","start":1205600,"end":1206520,"confidence":0.7050781,"speaker":"A"},{"text":"I","start":1206520,"end":1206760,"confidence":0.93896484,"speaker":"A"},{"text":"want","start":1206760,"end":1206840,"confidence":0.89404297,"speaker":"A"},{"text":"to","start":1206840,"end":1206920,"confidence":0.9980469,"speaker":"A"},{"text":"say","start":1206920,"end":1207040,"confidence":0.99609375,"speaker":"A"},{"text":"it","start":1207040,"end":1207160,"confidence":0.8076172,"speaker":"A"},{"text":"was","start":1207160,"end":1207400,"confidence":0.79248047,"speaker":"A"},{"text":"23.","start":1207480,"end":1208200,"confidence":0.99805,"speaker":"A"},{"text":"We","start":1210280,"end":1210600,"confidence":0.99853516,"speaker":"A"},{"text":"got","start":1210600,"end":1210840,"confidence":0.96240234,"speaker":"A"},{"text":"introduced","start":1210840,"end":1211360,"confidence":0.9744466,"speaker":"A"},{"text":"to","start":1211360,"end":1211520,"confidence":0.9995117,"speaker":"A"},{"text":"the","start":1211520,"end":1211680,"confidence":0.9995117,"speaker":"A"},{"text":"Open","start":1211680,"end":1211920,"confidence":0.9980469,"speaker":"A"},{"text":"API","start":1211920,"end":1212440,"confidence":0.97436523,"speaker":"A"},{"text":"generator","start":1212440,"end":1213000,"confidence":0.9851074,"speaker":"A"},{"text":"which","start":1213800,"end":1214000,"confidence":0.99365234,"speaker":"A"},{"text":"is","start":1214000,"end":1214320,"confidence":1,"speaker":"A"},{"text":"really","start":1214320,"end":1214600,"confidence":0.9995117,"speaker":"A"},{"text":"nice","start":1214600,"end":1215000,"confidence":1,"speaker":"A"},{"text":"because","start":1215000,"end":1215400,"confidence":0.99902344,"speaker":"A"},{"text":"then","start":1215960,"end":1216360,"confidence":0.9760742,"speaker":"A"},{"text":"we","start":1216840,"end":1217160,"confidence":0.6513672,"speaker":"A"},{"text":"have,","start":1217160,"end":1217480,"confidence":0.9902344,"speaker":"A"},{"text":"we","start":1217640,"end":1217920,"confidence":0.99609375,"speaker":"A"},{"text":"can","start":1217920,"end":1218080,"confidence":0.99902344,"speaker":"A"},{"text":"generate","start":1218080,"end":1218440,"confidence":0.99975586,"speaker":"A"},{"text":"the","start":1218440,"end":1218560,"confidence":0.9975586,"speaker":"A"},{"text":"Swift","start":1218560,"end":1218840,"confidence":0.7780762,"speaker":"A"},{"text":"code","start":1218840,"end":1219120,"confidence":0.96761066,"speaker":"A"},{"text":"if","start":1219120,"end":1219280,"confidence":1,"speaker":"A"},{"text":"we","start":1219280,"end":1219440,"confidence":0.99902344,"speaker":"A"},{"text":"know","start":1219440,"end":1219640,"confidence":0.98779297,"speaker":"A"},{"text":"what","start":1219640,"end":1219840,"confidence":0.99853516,"speaker":"A"},{"text":"the","start":1219840,"end":1220080,"confidence":0.9638672,"speaker":"A"},{"text":"Open","start":1220080,"end":1220400,"confidence":0.9980469,"speaker":"A"},{"text":"API","start":1220400,"end":1220880,"confidence":0.8979492,"speaker":"A"},{"text":"documentation","start":1220880,"end":1221720,"confidence":0.99970704,"speaker":"A"},{"text":"looks","start":1222200,"end":1222600,"confidence":1,"speaker":"A"},{"text":"like","start":1222600,"end":1222720,"confidence":0.99902344,"speaker":"A"},{"text":"it.","start":1222720,"end":1222880,"confidence":0.7519531,"speaker":"A"},{"text":"And","start":1222880,"end":1223040,"confidence":0.87597656,"speaker":"A"},{"text":"of","start":1223040,"end":1223160,"confidence":0.9980469,"speaker":"A"},{"text":"course","start":1223160,"end":1223280,"confidence":1,"speaker":"A"},{"text":"Apple","start":1223280,"end":1223600,"confidence":0.99975586,"speaker":"A"},{"text":"doesn't","start":1223600,"end":1223840,"confidence":0.99853516,"speaker":"A"},{"text":"provide","start":1223840,"end":1224080,"confidence":1,"speaker":"A"},{"text":"one","start":1224080,"end":1224320,"confidence":0.9926758,"speaker":"A"},{"text":"for","start":1224320,"end":1224480,"confidence":0.99902344,"speaker":"A"},{"text":"CloudKit","start":1224480,"end":1225240,"confidence":0.9314,"speaker":"A"},{"text":"but","start":1225960,"end":1226280,"confidence":0.9951172,"speaker":"A"},{"text":"they","start":1226280,"end":1226480,"confidence":0.88427734,"speaker":"A"},{"text":"did","start":1226480,"end":1226720,"confidence":0.98779297,"speaker":"A"},{"text":"provide","start":1226720,"end":1227040,"confidence":1,"speaker":"A"},{"text":"a","start":1227040,"end":1227280,"confidence":0.9995117,"speaker":"A"},{"text":"pretty","start":1227280,"end":1227520,"confidence":0.9998372,"speaker":"A"},{"text":"big","start":1227520,"end":1227720,"confidence":1,"speaker":"A"},{"text":"piece","start":1227720,"end":1228120,"confidence":0.99869794,"speaker":"A"},{"text":"open.","start":1229240,"end":1229639,"confidence":0.6689453,"speaker":"A"},{"text":"If","start":1229800,"end":1230040,"confidence":0.9873047,"speaker":"A"},{"text":"you","start":1230040,"end":1230120,"confidence":0.77490234,"speaker":"A"},{"text":"ever","start":1230120,"end":1230360,"confidence":0.91748047,"speaker":"A"},{"text":"you","start":1230360,"end":1230640,"confidence":0.7763672,"speaker":"A"},{"text":"looked","start":1230640,"end":1230920,"confidence":0.9987793,"speaker":"A"},{"text":"at","start":1230920,"end":1231000,"confidence":0.9995117,"speaker":"A"},{"text":"the","start":1231000,"end":1231120,"confidence":0.99902344,"speaker":"A"},{"text":"Open","start":1231120,"end":1231320,"confidence":0.99902344,"speaker":"A"},{"text":"API","start":1231320,"end":1231760,"confidence":0.9448242,"speaker":"A"},{"text":"generator,","start":1231760,"end":1232160,"confidence":0.9995117,"speaker":"A"},{"text":"it's","start":1232160,"end":1232400,"confidence":0.89192706,"speaker":"A"},{"text":"amazing.","start":1232400,"end":1232840,"confidence":0.9998372,"speaker":"A"},{"text":"Takes","start":1232840,"end":1233200,"confidence":0.7607422,"speaker":"A"},{"text":"the","start":1233200,"end":1233320,"confidence":0.46704102,"speaker":"A"},{"text":"Open","start":1233320,"end":1233520,"confidence":0.99902344,"speaker":"A"},{"text":"API","start":1233520,"end":1234080,"confidence":0.9501953,"speaker":"A"},{"text":"gamble","start":1234080,"end":1234640,"confidence":0.7845052,"speaker":"A"},{"text":"file","start":1234640,"end":1235000,"confidence":0.99121094,"speaker":"A"},{"text":"and","start":1235000,"end":1235320,"confidence":0.53125,"speaker":"A"},{"text":"generates","start":1235560,"end":1236160,"confidence":0.99975586,"speaker":"A"},{"text":"all","start":1236160,"end":1236400,"confidence":0.9995117,"speaker":"A"},{"text":"the","start":1236400,"end":1236560,"confidence":0.99609375,"speaker":"A"},{"text":"Swift","start":1236560,"end":1236840,"confidence":0.7429199,"speaker":"A"},{"text":"code","start":1236840,"end":1237080,"confidence":0.9991862,"speaker":"A"},{"text":"you","start":1237080,"end":1237240,"confidence":0.99853516,"speaker":"A"},{"text":"need.","start":1237240,"end":1237560,"confidence":1,"speaker":"A"},{"text":"One","start":1237880,"end":1238160,"confidence":0.99560547,"speaker":"A"},{"text":"of","start":1238160,"end":1238320,"confidence":0.9995117,"speaker":"A"},{"text":"the","start":1238320,"end":1238440,"confidence":1,"speaker":"A"},{"text":"other","start":1238440,"end":1238600,"confidence":0.99902344,"speaker":"A"},{"text":"issues","start":1238600,"end":1238880,"confidence":0.9995117,"speaker":"A"},{"text":"I","start":1238880,"end":1239120,"confidence":0.99902344,"speaker":"A"},{"text":"had","start":1239120,"end":1239280,"confidence":0.99658203,"speaker":"A"},{"text":"with","start":1239280,"end":1239560,"confidence":0.98828125,"speaker":"A"},{"text":"first","start":1240880,"end":1241040,"confidence":0.98339844,"speaker":"A"},{"text":"developing","start":1241040,"end":1241480,"confidence":0.99902344,"speaker":"A"},{"text":"Miskit","start":1241480,"end":1242160,"confidence":0.90844727,"speaker":"A"},{"text":"in","start":1242160,"end":1242440,"confidence":0.99072266,"speaker":"A"},{"text":"2020","start":1242440,"end":1243120,"confidence":0.99658,"speaker":"A"},{"text":"was","start":1243600,"end":1243920,"confidence":0.99609375,"speaker":"A"},{"text":"that","start":1243920,"end":1244160,"confidence":0.9951172,"speaker":"A"},{"text":"there","start":1244160,"end":1244360,"confidence":1,"speaker":"A"},{"text":"was","start":1244360,"end":1244520,"confidence":0.9995117,"speaker":"A"},{"text":"no","start":1244520,"end":1244720,"confidence":1,"speaker":"A"},{"text":"way","start":1244720,"end":1245000,"confidence":1,"speaker":"A"},{"text":"to","start":1245000,"end":1245320,"confidence":0.99658203,"speaker":"A"},{"text":"like","start":1245320,"end":1245680,"confidence":0.99072266,"speaker":"A"},{"text":"there","start":1245840,"end":1246160,"confidence":0.9770508,"speaker":"A"},{"text":"was","start":1246160,"end":1246360,"confidence":0.9941406,"speaker":"A"},{"text":"no","start":1246360,"end":1246520,"confidence":0.95410156,"speaker":"A"},{"text":"abstraction","start":1246520,"end":1247120,"confidence":0.9992676,"speaker":"A"},{"text":"layer","start":1247120,"end":1247520,"confidence":0.99934894,"speaker":"A"},{"text":"which","start":1247520,"end":1247800,"confidence":0.99902344,"speaker":"A"},{"text":"could","start":1247800,"end":1248040,"confidence":0.99316406,"speaker":"A"},{"text":"differentiate","start":1248040,"end":1248640,"confidence":0.9992676,"speaker":"A"},{"text":"between","start":1248640,"end":1248920,"confidence":1,"speaker":"A"},{"text":"doing","start":1248920,"end":1249200,"confidence":0.99902344,"speaker":"A"},{"text":"something","start":1249200,"end":1249440,"confidence":1,"speaker":"A"},{"text":"on","start":1249440,"end":1249640,"confidence":0.99902344,"speaker":"A"},{"text":"the","start":1249640,"end":1249800,"confidence":0.98876953,"speaker":"A"},{"text":"server","start":1249800,"end":1250320,"confidence":0.9995117,"speaker":"A"},{"text":"or","start":1250720,"end":1251080,"confidence":0.99902344,"speaker":"A"},{"text":"using","start":1251080,"end":1251440,"confidence":0.9975586,"speaker":"A"},{"text":"regular","start":1251760,"end":1252400,"confidence":0.99902344,"speaker":"A"},{"text":"like","start":1252480,"end":1252880,"confidence":0.9765625,"speaker":"A"},{"text":"URL","start":1253040,"end":1253680,"confidence":0.9951172,"speaker":"A"},{"text":"session","start":1253680,"end":1254040,"confidence":0.9991862,"speaker":"A"},{"text":"which","start":1254040,"end":1254200,"confidence":0.9995117,"speaker":"A"},{"text":"is","start":1254200,"end":1254360,"confidence":0.99658203,"speaker":"A"},{"text":"more","start":1254360,"end":1254600,"confidence":1,"speaker":"A"},{"text":"targeted","start":1254600,"end":1255080,"confidence":1,"speaker":"A"},{"text":"towards","start":1255080,"end":1255360,"confidence":0.9992676,"speaker":"A"},{"text":"client","start":1255360,"end":1255719,"confidence":0.9328613,"speaker":"A"},{"text":"side.","start":1255719,"end":1256080,"confidence":0.99853516,"speaker":"A"},{"text":"So","start":1258960,"end":1259360,"confidence":0.9970703,"speaker":"A"},{"text":"I","start":1259440,"end":1259720,"confidence":0.99121094,"speaker":"A"},{"text":"had","start":1259720,"end":1259880,"confidence":0.8510742,"speaker":"A"},{"text":"to","start":1259880,"end":1260000,"confidence":0.97216797,"speaker":"A"},{"text":"build","start":1260000,"end":1260120,"confidence":0.9970703,"speaker":"A"},{"text":"my","start":1260120,"end":1260280,"confidence":0.9995117,"speaker":"A"},{"text":"own","start":1260280,"end":1260440,"confidence":1,"speaker":"A"},{"text":"abstraction","start":1260440,"end":1261000,"confidence":0.90441895,"speaker":"A"},{"text":"for","start":1261000,"end":1261120,"confidence":1,"speaker":"A"},{"text":"that.","start":1261120,"end":1261280,"confidence":1,"speaker":"A"},{"text":"Luckily","start":1261280,"end":1261640,"confidence":0.99641925,"speaker":"A"},{"text":"Open","start":1261640,"end":1261840,"confidence":0.9995117,"speaker":"A"},{"text":"API","start":1261840,"end":1262440,"confidence":0.7475586,"speaker":"A"},{"text":"has,","start":1262440,"end":1262800,"confidence":0.99609375,"speaker":"A"},{"text":"there's","start":1264080,"end":1264560,"confidence":0.99820966,"speaker":"A"},{"text":"open","start":1264560,"end":1264880,"confidence":0.87109375,"speaker":"A"},{"text":"API","start":1264960,"end":1265600,"confidence":0.8029785,"speaker":"A"},{"text":"transport","start":1265600,"end":1266240,"confidence":0.99902344,"speaker":"A"},{"text":"I","start":1266240,"end":1266520,"confidence":0.99658203,"speaker":"A"},{"text":"believe,","start":1266520,"end":1266800,"confidence":0.9995117,"speaker":"A"},{"text":"which","start":1266880,"end":1267240,"confidence":0.9995117,"speaker":"A"},{"text":"provides","start":1267240,"end":1267600,"confidence":0.9995117,"speaker":"A"},{"text":"an","start":1267600,"end":1267720,"confidence":0.99121094,"speaker":"A"},{"text":"abstraction","start":1267720,"end":1268400,"confidence":0.98132324,"speaker":"A"},{"text":"layer","start":1268480,"end":1268840,"confidence":0.96940106,"speaker":"A"},{"text":"where","start":1268840,"end":1269000,"confidence":0.9995117,"speaker":"A"},{"text":"you","start":1269000,"end":1269120,"confidence":1,"speaker":"A"},{"text":"can","start":1269120,"end":1269240,"confidence":0.9995117,"speaker":"A"},{"text":"then","start":1269240,"end":1269400,"confidence":0.9975586,"speaker":"A"},{"text":"plug","start":1269400,"end":1269640,"confidence":0.9992676,"speaker":"A"},{"text":"in","start":1269640,"end":1269840,"confidence":0.9946289,"speaker":"A"},{"text":"either","start":1269840,"end":1270120,"confidence":0.9980469,"speaker":"A"},{"text":"use","start":1270120,"end":1270400,"confidence":0.99316406,"speaker":"A"},{"text":"Async","start":1270980,"end":1271420,"confidence":0.94433594,"speaker":"A"},{"text":"HTTP","start":1271420,"end":1272100,"confidence":0.9790039,"speaker":"A"},{"text":"client,","start":1272100,"end":1272620,"confidence":0.9975586,"speaker":"A"},{"text":"which","start":1272620,"end":1272900,"confidence":0.9995117,"speaker":"A"},{"text":"is","start":1272900,"end":1273140,"confidence":0.9995117,"speaker":"A"},{"text":"the","start":1273140,"end":1273420,"confidence":0.9995117,"speaker":"A"},{"text":"server","start":1273420,"end":1273900,"confidence":0.99902344,"speaker":"A"},{"text":"way","start":1273900,"end":1274060,"confidence":0.98583984,"speaker":"A"},{"text":"of","start":1274060,"end":1274220,"confidence":1,"speaker":"A"},{"text":"doing","start":1274220,"end":1274380,"confidence":1,"speaker":"A"},{"text":"it,","start":1274380,"end":1274540,"confidence":0.9995117,"speaker":"A"},{"text":"or","start":1274540,"end":1274780,"confidence":0.59228516,"speaker":"A"},{"text":"you","start":1274780,"end":1275020,"confidence":0.9995117,"speaker":"A"},{"text":"can","start":1275020,"end":1275180,"confidence":0.9995117,"speaker":"A"},{"text":"plug","start":1275180,"end":1275380,"confidence":0.99975586,"speaker":"A"},{"text":"in","start":1275380,"end":1275500,"confidence":0.99658203,"speaker":"A"},{"text":"a","start":1275500,"end":1275660,"confidence":0.99609375,"speaker":"A"},{"text":"URL","start":1275660,"end":1276180,"confidence":0.99853516,"speaker":"A"},{"text":"session","start":1276180,"end":1276660,"confidence":0.87906903,"speaker":"A"},{"text":"transport,","start":1277060,"end":1277780,"confidence":0.99902344,"speaker":"A"},{"text":"which","start":1277860,"end":1278180,"confidence":0.9995117,"speaker":"A"},{"text":"is","start":1278180,"end":1278500,"confidence":0.9995117,"speaker":"A"},{"text":"of","start":1278500,"end":1278780,"confidence":0.5307617,"speaker":"A"},{"text":"course","start":1278780,"end":1278940,"confidence":0.9995117,"speaker":"A"},{"text":"the","start":1278940,"end":1279100,"confidence":0.5600586,"speaker":"A"},{"text":"client","start":1279100,"end":1279380,"confidence":0.99487305,"speaker":"A"},{"text":"way","start":1279380,"end":1279580,"confidence":0.9941406,"speaker":"A"},{"text":"to","start":1279580,"end":1279700,"confidence":0.9995117,"speaker":"A"},{"text":"do,","start":1279700,"end":1279820,"confidence":0.9995117,"speaker":"A"},{"text":"provides","start":1282060,"end":1282420,"confidence":0.9995117,"speaker":"A"},{"text":"a","start":1282420,"end":1282540,"confidence":0.9995117,"speaker":"A"},{"text":"really","start":1282540,"end":1282700,"confidence":0.9995117,"speaker":"A"},{"text":"great","start":1282700,"end":1282980,"confidence":0.9995117,"speaker":"A"},{"text":"tutorial.","start":1283060,"end":1283740,"confidence":0.9855957,"speaker":"A"},{"text":"I","start":1283740,"end":1283980,"confidence":0.96777344,"speaker":"A"},{"text":"highly","start":1283980,"end":1284300,"confidence":0.998291,"speaker":"A"},{"text":"recommend","start":1284300,"end":1284620,"confidence":1,"speaker":"A"},{"text":"checking","start":1284620,"end":1284900,"confidence":0.99934894,"speaker":"A"},{"text":"this","start":1284900,"end":1285060,"confidence":0.9951172,"speaker":"A"},{"text":"out","start":1285060,"end":1285380,"confidence":0.9970703,"speaker":"A"},{"text":"as","start":1286579,"end":1286859,"confidence":1,"speaker":"A"},{"text":"well","start":1286859,"end":1287020,"confidence":1,"speaker":"A"},{"text":"as","start":1287020,"end":1287300,"confidence":0.99853516,"speaker":"A"},{"text":"the","start":1287380,"end":1287740,"confidence":0.9975586,"speaker":"A"},{"text":"doxy","start":1287740,"end":1288340,"confidence":0.84684247,"speaker":"A"},{"text":"documentation","start":1288340,"end":1289060,"confidence":0.99990237,"speaker":"A"},{"text":"that","start":1289220,"end":1289500,"confidence":0.99853516,"speaker":"A"},{"text":"they","start":1289500,"end":1289700,"confidence":0.9995117,"speaker":"A"},{"text":"provide.","start":1289700,"end":1290020,"confidence":0.9970703,"speaker":"A"},{"text":"So","start":1291860,"end":1292220,"confidence":0.9667969,"speaker":"A"},{"text":"this","start":1292220,"end":1292460,"confidence":0.9995117,"speaker":"A"},{"text":"is","start":1292460,"end":1292660,"confidence":0.95654297,"speaker":"A"},{"text":"great.","start":1292660,"end":1292940,"confidence":1,"speaker":"A"},{"text":"But","start":1292940,"end":1293180,"confidence":0.99609375,"speaker":"A"},{"text":"then","start":1293180,"end":1293420,"confidence":0.99853516,"speaker":"A"},{"text":"I'd","start":1293420,"end":1293820,"confidence":0.99625653,"speaker":"A"},{"text":"have","start":1293820,"end":1293980,"confidence":0.9995117,"speaker":"A"},{"text":"to","start":1293980,"end":1294100,"confidence":1,"speaker":"A"},{"text":"go","start":1294100,"end":1294220,"confidence":0.9995117,"speaker":"A"},{"text":"ahead","start":1294220,"end":1294500,"confidence":0.9995117,"speaker":"A"},{"text":"and","start":1294660,"end":1294940,"confidence":0.99853516,"speaker":"A"},{"text":"I'd","start":1294940,"end":1295180,"confidence":0.8806966,"speaker":"A"},{"text":"have","start":1295180,"end":1295300,"confidence":0.9995117,"speaker":"A"},{"text":"to","start":1295300,"end":1295420,"confidence":0.9995117,"speaker":"A"},{"text":"figure","start":1295420,"end":1295660,"confidence":0.7961426,"speaker":"A"},{"text":"out","start":1295660,"end":1295820,"confidence":0.99853516,"speaker":"A"},{"text":"a","start":1295820,"end":1295980,"confidence":0.9970703,"speaker":"A"},{"text":"way","start":1295980,"end":1296260,"confidence":0.99560547,"speaker":"A"},{"text":"to","start":1296900,"end":1297020,"confidence":0.9819336,"speaker":"A"},{"text":"convert","start":1297020,"end":1297300,"confidence":0.9992676,"speaker":"A"},{"text":"all","start":1297300,"end":1297540,"confidence":0.9995117,"speaker":"A"},{"text":"this","start":1297540,"end":1297740,"confidence":0.9975586,"speaker":"A"},{"text":"documentation","start":1297740,"end":1298500,"confidence":0.9995117,"speaker":"A"},{"text":"into","start":1298660,"end":1299060,"confidence":0.9995117,"speaker":"A"},{"text":"an","start":1299140,"end":1299420,"confidence":0.99853516,"speaker":"A"},{"text":"open","start":1299420,"end":1299700,"confidence":0.9995117,"speaker":"A"},{"text":"API","start":1299700,"end":1300340,"confidence":0.9458008,"speaker":"A"},{"text":"document.","start":1300420,"end":1301140,"confidence":0.9998779,"speaker":"A"},{"text":"I","start":1302420,"end":1302700,"confidence":0.5463867,"speaker":"A"},{"text":"mean,","start":1302700,"end":1302860,"confidence":0.9926758,"speaker":"A"},{"text":"can","start":1302860,"end":1303020,"confidence":0.9995117,"speaker":"A"},{"text":"you","start":1303020,"end":1303180,"confidence":0.99902344,"speaker":"A"},{"text":"guess","start":1303180,"end":1303540,"confidence":0.99975586,"speaker":"A"},{"text":"what","start":1303940,"end":1304260,"confidence":0.9995117,"speaker":"A"},{"text":"helped","start":1304260,"end":1304620,"confidence":0.76538086,"speaker":"A"},{"text":"me","start":1304620,"end":1304980,"confidence":0.9926758,"speaker":"A"},{"text":"to","start":1305540,"end":1305820,"confidence":0.9873047,"speaker":"A"},{"text":"get","start":1305820,"end":1306100,"confidence":0.6230469,"speaker":"A"},{"text":"build","start":1306180,"end":1306580,"confidence":0.95996094,"speaker":"A"},{"text":"an","start":1306820,"end":1307100,"confidence":0.9550781,"speaker":"A"},{"text":"open","start":1307100,"end":1307340,"confidence":0.9995117,"speaker":"A"},{"text":"API","start":1307340,"end":1307860,"confidence":0.90722656,"speaker":"A"},{"text":"document","start":1307860,"end":1308260,"confidence":0.9959717,"speaker":"A"},{"text":"from","start":1308260,"end":1308460,"confidence":0.99853516,"speaker":"A"},{"text":"all","start":1308460,"end":1308620,"confidence":0.9995117,"speaker":"A"},{"text":"this","start":1308620,"end":1308820,"confidence":0.9555664,"speaker":"A"},{"text":"documentation?","start":1308820,"end":1309540,"confidence":0.9988281,"speaker":"A"},{"text":"Some","start":1310340,"end":1310740,"confidence":0.62402344,"speaker":"B"},{"text":"of","start":1311060,"end":1311260,"confidence":0.25683594,"speaker":"B"},{"text":"the","start":1311260,"end":1311300,"confidence":0.56347656,"speaker":"B"},{"text":"tools,","start":1311300,"end":1311620,"confidence":0.72314453,"speaker":"B"},{"text":"some","start":1312659,"end":1312940,"confidence":0.9658203,"speaker":"B"},{"text":"AI","start":1312940,"end":1313260,"confidence":0.9914551,"speaker":"B"},{"text":"tool.","start":1313260,"end":1313540,"confidence":0.9716797,"speaker":"B"},{"text":"Yes.","start":1314500,"end":1314980,"confidence":0.9482422,"speaker":"A"},{"text":"AI","start":1316820,"end":1317340,"confidence":0.91967773,"speaker":"A"},{"text":"came","start":1317340,"end":1317620,"confidence":0.9980469,"speaker":"A"},{"text":"and","start":1317620,"end":1317900,"confidence":0.99853516,"speaker":"A"},{"text":"I'm","start":1317900,"end":1318140,"confidence":0.99934894,"speaker":"A"},{"text":"like,","start":1318140,"end":1318340,"confidence":0.9921875,"speaker":"A"},{"text":"holy","start":1318340,"end":1318620,"confidence":0.82543945,"speaker":"A"},{"text":"crap.","start":1318620,"end":1318980,"confidence":0.86450195,"speaker":"A"},{"text":"Like","start":1319460,"end":1319860,"confidence":0.6220703,"speaker":"A"},{"text":"AI","start":1320180,"end":1320660,"confidence":0.92407227,"speaker":"A"},{"text":"is","start":1320660,"end":1320860,"confidence":0.9946289,"speaker":"A"},{"text":"really","start":1320860,"end":1321020,"confidence":0.99902344,"speaker":"A"},{"text":"good","start":1321020,"end":1321180,"confidence":0.99902344,"speaker":"A"},{"text":"at","start":1321180,"end":1321340,"confidence":0.9995117,"speaker":"A"},{"text":"documenting","start":1321340,"end":1321820,"confidence":0.99990237,"speaker":"A"},{"text":"your","start":1321820,"end":1321980,"confidence":0.99902344,"speaker":"A"},{"text":"code,","start":1321980,"end":1322260,"confidence":0.9998372,"speaker":"A"},{"text":"but","start":1322260,"end":1322460,"confidence":0.96972656,"speaker":"A"},{"text":"it's","start":1322460,"end":1322660,"confidence":0.9749349,"speaker":"A"},{"text":"also","start":1322660,"end":1322820,"confidence":0.9995117,"speaker":"A"},{"text":"pretty","start":1322820,"end":1323060,"confidence":0.9996745,"speaker":"A"},{"text":"darn","start":1323060,"end":1323260,"confidence":0.90804034,"speaker":"A"},{"text":"good","start":1323260,"end":1323420,"confidence":1,"speaker":"A"},{"text":"at","start":1323420,"end":1323700,"confidence":0.9902344,"speaker":"A"},{"text":"taking","start":1324490,"end":1324690,"confidence":0.93066406,"speaker":"A"},{"text":"documentation","start":1324690,"end":1325370,"confidence":0.9998047,"speaker":"A"},{"text":"and","start":1325370,"end":1325570,"confidence":0.99609375,"speaker":"A"},{"text":"building","start":1325570,"end":1325810,"confidence":0.9995117,"speaker":"A"},{"text":"code.","start":1325810,"end":1326250,"confidence":0.8733724,"speaker":"A"},{"text":"So","start":1326890,"end":1327170,"confidence":0.9238281,"speaker":"A"},{"text":"then","start":1327170,"end":1327450,"confidence":0.99658203,"speaker":"A"},{"text":"I","start":1327930,"end":1328250,"confidence":0.9819336,"speaker":"A"},{"text":"would","start":1328250,"end":1328450,"confidence":0.9848633,"speaker":"A"},{"text":"just","start":1328450,"end":1328610,"confidence":0.99902344,"speaker":"A"},{"text":"plug","start":1328610,"end":1328850,"confidence":0.9938965,"speaker":"A"},{"text":"it.","start":1328850,"end":1329050,"confidence":0.8227539,"speaker":"A"},{"text":"I've","start":1329050,"end":1329290,"confidence":0.99397784,"speaker":"A"},{"text":"been","start":1329290,"end":1329410,"confidence":0.9975586,"speaker":"A"},{"text":"plugging","start":1329410,"end":1329730,"confidence":0.95751953,"speaker":"A"},{"text":"in","start":1329730,"end":1329890,"confidence":0.8691406,"speaker":"A"},{"text":"with","start":1329890,"end":1330050,"confidence":0.9995117,"speaker":"A"},{"text":"Claude","start":1330050,"end":1330650,"confidence":0.73999023,"speaker":"A"},{"text":"and","start":1331050,"end":1331330,"confidence":0.9667969,"speaker":"A"},{"text":"it","start":1331330,"end":1331490,"confidence":0.9975586,"speaker":"A"},{"text":"has","start":1331490,"end":1331650,"confidence":1,"speaker":"A"},{"text":"a","start":1331650,"end":1331850,"confidence":0.9995117,"speaker":"A"},{"text":"copy","start":1331850,"end":1332170,"confidence":1,"speaker":"A"},{"text":"of","start":1332170,"end":1332290,"confidence":1,"speaker":"A"},{"text":"all","start":1332290,"end":1332450,"confidence":0.9995117,"speaker":"A"},{"text":"the","start":1332450,"end":1332610,"confidence":0.9995117,"speaker":"A"},{"text":"documentation","start":1332610,"end":1333210,"confidence":0.99970704,"speaker":"A"},{"text":"in","start":1333210,"end":1333410,"confidence":0.9277344,"speaker":"A"},{"text":"my","start":1333410,"end":1333570,"confidence":1,"speaker":"A"},{"text":"repo","start":1333570,"end":1334090,"confidence":0.9848633,"speaker":"A"},{"text":"and","start":1334410,"end":1334730,"confidence":0.9682617,"speaker":"A"},{"text":"it","start":1334730,"end":1334930,"confidence":0.8828125,"speaker":"A"},{"text":"can","start":1334930,"end":1335090,"confidence":0.9995117,"speaker":"A"},{"text":"go","start":1335090,"end":1335250,"confidence":0.9995117,"speaker":"A"},{"text":"ahead","start":1335250,"end":1335410,"confidence":0.9995117,"speaker":"A"},{"text":"and","start":1335410,"end":1335610,"confidence":0.99853516,"speaker":"A"},{"text":"edit","start":1335610,"end":1336090,"confidence":0.9995117,"speaker":"A"},{"text":"the","start":1336250,"end":1336490,"confidence":0.9824219,"speaker":"A"},{"text":"open","start":1336490,"end":1336690,"confidence":0.99316406,"speaker":"A"},{"text":"API.","start":1336690,"end":1337210,"confidence":0.9802246,"speaker":"A"},{"text":"It's","start":1337210,"end":1337490,"confidence":0.9817708,"speaker":"A"},{"text":"not","start":1337490,"end":1337690,"confidence":0.99853516,"speaker":"A"},{"text":"perfect","start":1337690,"end":1338010,"confidence":0.97998047,"speaker":"A"},{"text":"by","start":1338010,"end":1338250,"confidence":0.99853516,"speaker":"A"},{"text":"any","start":1338250,"end":1338490,"confidence":1,"speaker":"A"},{"text":"means,","start":1338490,"end":1338810,"confidence":1,"speaker":"A"},{"text":"of","start":1338810,"end":1339090,"confidence":0.99902344,"speaker":"A"},{"text":"course,","start":1339090,"end":1339370,"confidence":1,"speaker":"A"},{"text":"but","start":1339530,"end":1339849,"confidence":0.9970703,"speaker":"A"},{"text":"that's","start":1339849,"end":1340170,"confidence":0.9998372,"speaker":"A"},{"text":"what","start":1340170,"end":1340410,"confidence":0.9980469,"speaker":"A"},{"text":"unit","start":1340410,"end":1340850,"confidence":0.84521484,"speaker":"A"},{"text":"tests","start":1340850,"end":1341210,"confidence":0.9946289,"speaker":"A"},{"text":"are","start":1341210,"end":1341330,"confidence":0.99560547,"speaker":"A"},{"text":"for.","start":1341330,"end":1341610,"confidence":0.99658203,"speaker":"A"},{"text":"And","start":1343850,"end":1344170,"confidence":0.89697266,"speaker":"A"},{"text":"actually","start":1344170,"end":1344410,"confidence":0.99853516,"speaker":"A"},{"text":"having","start":1344410,"end":1344650,"confidence":0.87402344,"speaker":"A"},{"text":"integration","start":1344650,"end":1345210,"confidence":0.9769287,"speaker":"A"},{"text":"tests","start":1345210,"end":1345770,"confidence":0.9975586,"speaker":"A"},{"text":"in","start":1346250,"end":1346530,"confidence":0.99853516,"speaker":"A"},{"text":"order","start":1346530,"end":1346730,"confidence":1,"speaker":"A"},{"text":"to","start":1346730,"end":1346930,"confidence":0.9995117,"speaker":"A"},{"text":"do","start":1346930,"end":1347130,"confidence":0.9995117,"speaker":"A"},{"text":"stuff","start":1347130,"end":1347530,"confidence":0.9998372,"speaker":"A"},{"text":"so","start":1347690,"end":1348090,"confidence":0.83496094,"speaker":"A"},{"text":"that.","start":1351460,"end":1351700,"confidence":0.9980469,"speaker":"A"},{"text":"Sorry,","start":1355380,"end":1355740,"confidence":0.9995117,"speaker":"A"},{"text":"I","start":1355740,"end":1355860,"confidence":0.9995117,"speaker":"A"},{"text":"just","start":1355860,"end":1355980,"confidence":1,"speaker":"A"},{"text":"want","start":1355980,"end":1356140,"confidence":0.99560547,"speaker":"A"},{"text":"to","start":1356140,"end":1356300,"confidence":0.99365234,"speaker":"A"},{"text":"make","start":1356300,"end":1356460,"confidence":1,"speaker":"A"},{"text":"sure","start":1356460,"end":1356740,"confidence":1,"speaker":"A"},{"text":"nothing","start":1360660,"end":1361100,"confidence":0.88623047,"speaker":"A"},{"text":"important.","start":1361100,"end":1361460,"confidence":1,"speaker":"A"},{"text":"I","start":1366900,"end":1367180,"confidence":0.9951172,"speaker":"A"},{"text":"hate","start":1367180,"end":1367460,"confidence":0.9992676,"speaker":"A"},{"text":"teams.","start":1367460,"end":1368020,"confidence":0.9995117,"speaker":"A"},{"text":"Okay,","start":1373060,"end":1373620,"confidence":0.94677734,"speaker":"A"},{"text":"so","start":1374820,"end":1375100,"confidence":0.9980469,"speaker":"A"},{"text":"great.","start":1375100,"end":1375380,"confidence":0.9980469,"speaker":"A"},{"text":"So","start":1375700,"end":1375780,"confidence":0.9995117,"speaker":"A"},{"text":"let's","start":1375780,"end":1375980,"confidence":0.9996745,"speaker":"A"},{"text":"talk","start":1375980,"end":1376140,"confidence":0.9995117,"speaker":"A"},{"text":"about.","start":1376140,"end":1376420,"confidence":0.9980469,"speaker":"A"},{"text":"Sorry,","start":1379700,"end":1380180,"confidence":0.90966797,"speaker":"A"},{"text":"slides","start":1380500,"end":1380900,"confidence":0.76538086,"speaker":"A"},{"text":"are","start":1380900,"end":1381100,"confidence":0.9995117,"speaker":"A"},{"text":"still","start":1381100,"end":1381260,"confidence":1,"speaker":"A"},{"text":"not","start":1381260,"end":1381420,"confidence":1,"speaker":"A"},{"text":"done,","start":1381420,"end":1381620,"confidence":0.9980469,"speaker":"A"},{"text":"but","start":1381620,"end":1381940,"confidence":0.99316406,"speaker":"A"},{"text":"let's","start":1382100,"end":1382460,"confidence":0.9991862,"speaker":"A"},{"text":"talk","start":1382460,"end":1382620,"confidence":0.9995117,"speaker":"A"},{"text":"about","start":1382620,"end":1382900,"confidence":0.9980469,"speaker":"A"},{"text":"authentication","start":1384500,"end":1385380,"confidence":1,"speaker":"A"},{"text":"methods.","start":1385380,"end":1386020,"confidence":0.99975586,"speaker":"A"},{"text":"You","start":1386340,"end":1386620,"confidence":0.9970703,"speaker":"A"},{"text":"can","start":1386620,"end":1386780,"confidence":0.8959961,"speaker":"A"},{"text":"see","start":1386780,"end":1386940,"confidence":0.9995117,"speaker":"A"},{"text":"I","start":1386940,"end":1387100,"confidence":0.99902344,"speaker":"A"},{"text":"have","start":1387100,"end":1387380,"confidence":0.9995117,"speaker":"A"},{"text":"the","start":1387460,"end":1387740,"confidence":0.99121094,"speaker":"A"},{"text":"logos","start":1387740,"end":1388140,"confidence":0.9980469,"speaker":"A"},{"text":"here,","start":1388140,"end":1388300,"confidence":0.9995117,"speaker":"A"},{"text":"but","start":1388300,"end":1388420,"confidence":1,"speaker":"A"},{"text":"I","start":1388420,"end":1388540,"confidence":0.9995117,"speaker":"A"},{"text":"haven't","start":1388540,"end":1388780,"confidence":0.99975586,"speaker":"A"},{"text":"quite","start":1388780,"end":1389020,"confidence":0.99975586,"speaker":"A"},{"text":"cleaned","start":1389020,"end":1389340,"confidence":0.79541016,"speaker":"A"},{"text":"this","start":1389340,"end":1389540,"confidence":0.9941406,"speaker":"A"},{"text":"up.","start":1389540,"end":1389860,"confidence":0.9970703,"speaker":"A"},{"text":"So","start":1390820,"end":1391220,"confidence":0.9770508,"speaker":"A"},{"text":"there's","start":1391940,"end":1392540,"confidence":0.9983724,"speaker":"A"},{"text":"really","start":1392540,"end":1392900,"confidence":0.99902344,"speaker":"A"},{"text":"two","start":1393780,"end":1394140,"confidence":1,"speaker":"A"},{"text":"and","start":1394140,"end":1394380,"confidence":0.87890625,"speaker":"A"},{"text":"a","start":1394380,"end":1394540,"confidence":0.9667969,"speaker":"A"},{"text":"half","start":1394540,"end":1394820,"confidence":0.9995117,"speaker":"A"},{"text":"authentication","start":1394820,"end":1395660,"confidence":0.99975586,"speaker":"A"},{"text":"methods","start":1395660,"end":1396140,"confidence":1,"speaker":"A"},{"text":"when","start":1396140,"end":1396300,"confidence":1,"speaker":"A"},{"text":"it","start":1396300,"end":1396420,"confidence":1,"speaker":"A"},{"text":"comes","start":1396420,"end":1396540,"confidence":1,"speaker":"A"},{"text":"to","start":1396540,"end":1396700,"confidence":1,"speaker":"A"},{"text":"CloudKit.","start":1396700,"end":1397380,"confidence":0.9552,"speaker":"A"},{"text":"So","start":1398420,"end":1398820,"confidence":0.9326172,"speaker":"A"},{"text":"here","start":1398900,"end":1399300,"confidence":0.99853516,"speaker":"A"},{"text":"is","start":1399460,"end":1399860,"confidence":0.9658203,"speaker":"A"},{"text":"the","start":1401150,"end":1401270,"confidence":0.95947266,"speaker":"A"},{"text":"miss","start":1401270,"end":1401470,"confidence":0.5654297,"speaker":"A"},{"text":"demo","start":1401470,"end":1401950,"confidence":0.7548828,"speaker":"A"},{"text":"database.","start":1401950,"end":1402630,"confidence":0.9996745,"speaker":"A"},{"text":"You","start":1402630,"end":1402870,"confidence":0.9995117,"speaker":"A"},{"text":"just","start":1402870,"end":1403030,"confidence":0.9995117,"speaker":"A"},{"text":"go","start":1403030,"end":1403230,"confidence":0.9995117,"speaker":"A"},{"text":"in","start":1403230,"end":1403430,"confidence":0.9995117,"speaker":"A"},{"text":"here","start":1403430,"end":1403710,"confidence":0.9995117,"speaker":"A"},{"text":"and","start":1404270,"end":1404550,"confidence":0.99560547,"speaker":"A"},{"text":"you","start":1404550,"end":1404710,"confidence":0.99853516,"speaker":"A"},{"text":"can","start":1404710,"end":1404870,"confidence":0.99365234,"speaker":"A"},{"text":"go","start":1404870,"end":1404990,"confidence":1,"speaker":"A"},{"text":"to","start":1404990,"end":1405110,"confidence":0.9995117,"speaker":"A"},{"text":"tokens","start":1405110,"end":1405510,"confidence":0.99975586,"speaker":"A"},{"text":"and","start":1405510,"end":1405670,"confidence":0.9892578,"speaker":"A"},{"text":"keys","start":1405670,"end":1406070,"confidence":0.9992676,"speaker":"A"},{"text":"and","start":1406070,"end":1406310,"confidence":0.9975586,"speaker":"A"},{"text":"then","start":1406310,"end":1406470,"confidence":0.99902344,"speaker":"A"},{"text":"that","start":1406470,"end":1406630,"confidence":0.9995117,"speaker":"A"},{"text":"will","start":1406630,"end":1406790,"confidence":0.9995117,"speaker":"A"},{"text":"give","start":1406790,"end":1406950,"confidence":1,"speaker":"A"},{"text":"you","start":1406950,"end":1407150,"confidence":1,"speaker":"A"},{"text":"access","start":1407150,"end":1407470,"confidence":1,"speaker":"A"},{"text":"to","start":1407470,"end":1407750,"confidence":0.98339844,"speaker":"A"},{"text":"set","start":1407750,"end":1407950,"confidence":0.99658203,"speaker":"A"},{"text":"up","start":1407950,"end":1408270,"confidence":0.7631836,"speaker":"A"},{"text":"either","start":1408510,"end":1408990,"confidence":0.99975586,"speaker":"A"},{"text":"the","start":1408990,"end":1409390,"confidence":0.9995117,"speaker":"A"},{"text":"API","start":1409870,"end":1410550,"confidence":0.9995117,"speaker":"A"},{"text":"if","start":1410550,"end":1410750,"confidence":0.9995117,"speaker":"A"},{"text":"you","start":1410750,"end":1410870,"confidence":0.9243164,"speaker":"A"},{"text":"want","start":1410870,"end":1411030,"confidence":0.94921875,"speaker":"A"},{"text":"to","start":1411030,"end":1411150,"confidence":0.9980469,"speaker":"A"},{"text":"do","start":1411150,"end":1411390,"confidence":0.9970703,"speaker":"A"},{"text":"API","start":1411790,"end":1412430,"confidence":0.9926758,"speaker":"A"},{"text":"key","start":1412430,"end":1412830,"confidence":0.9995117,"speaker":"A"},{"text":"or","start":1412830,"end":1413110,"confidence":0.9980469,"speaker":"A"},{"text":"API","start":1413110,"end":1413470,"confidence":0.8027344,"speaker":"A"},{"text":"token","start":1413470,"end":1414030,"confidence":0.86376953,"speaker":"A"},{"text":"if","start":1414270,"end":1414550,"confidence":0.9995117,"speaker":"A"},{"text":"you","start":1414550,"end":1414710,"confidence":1,"speaker":"A"},{"text":"want","start":1414710,"end":1414830,"confidence":0.9394531,"speaker":"A"},{"text":"to","start":1414830,"end":1414910,"confidence":0.99902344,"speaker":"A"},{"text":"do","start":1414910,"end":1415070,"confidence":0.99902344,"speaker":"A"},{"text":"a","start":1415070,"end":1415270,"confidence":0.53125,"speaker":"A"},{"text":"private","start":1415270,"end":1415470,"confidence":1,"speaker":"A"},{"text":"database","start":1415470,"end":1416190,"confidence":0.9998372,"speaker":"A"},{"text":"or","start":1416190,"end":1416550,"confidence":0.9995117,"speaker":"A"},{"text":"a","start":1416550,"end":1416790,"confidence":0.99853516,"speaker":"A"},{"text":"server","start":1416790,"end":1417109,"confidence":0.9946289,"speaker":"A"},{"text":"to","start":1417109,"end":1417310,"confidence":0.97753906,"speaker":"A"},{"text":"server","start":1417310,"end":1417630,"confidence":0.9992676,"speaker":"A"},{"text":"keyset","start":1417630,"end":1418190,"confidence":0.8388672,"speaker":"A"},{"text":"if","start":1418350,"end":1418630,"confidence":0.9995117,"speaker":"A"},{"text":"you","start":1418630,"end":1418750,"confidence":0.99902344,"speaker":"A"},{"text":"want","start":1418750,"end":1418870,"confidence":0.53808594,"speaker":"A"},{"text":"to","start":1418870,"end":1418990,"confidence":0.9951172,"speaker":"A"},{"text":"do","start":1418990,"end":1419150,"confidence":0.99902344,"speaker":"A"},{"text":"a","start":1419150,"end":1419310,"confidence":0.8515625,"speaker":"A"},{"text":"public","start":1419310,"end":1419470,"confidence":1,"speaker":"A"},{"text":"database.","start":1419470,"end":1420190,"confidence":0.9996745,"speaker":"A"},{"text":"So","start":1420190,"end":1420430,"confidence":0.98095703,"speaker":"A"},{"text":"let's","start":1420430,"end":1420590,"confidence":0.9998372,"speaker":"A"},{"text":"talk","start":1420590,"end":1420710,"confidence":0.99902344,"speaker":"A"},{"text":"about","start":1420710,"end":1420870,"confidence":0.99902344,"speaker":"A"},{"text":"the","start":1420870,"end":1421030,"confidence":0.9980469,"speaker":"A"},{"text":"API","start":1421030,"end":1421430,"confidence":0.99902344,"speaker":"A"},{"text":"token.","start":1421430,"end":1421950,"confidence":0.9773763,"speaker":"A"},{"text":"Pretty","start":1422510,"end":1422870,"confidence":1,"speaker":"A"},{"text":"simple.","start":1422870,"end":1423310,"confidence":0.83935547,"speaker":"A"},{"text":"You","start":1423470,"end":1423750,"confidence":0.9995117,"speaker":"A"},{"text":"just","start":1423750,"end":1423870,"confidence":1,"speaker":"A"},{"text":"go","start":1423870,"end":1423990,"confidence":0.99609375,"speaker":"A"},{"text":"into","start":1423990,"end":1424190,"confidence":0.61572266,"speaker":"A"},{"text":"here,","start":1424190,"end":1424510,"confidence":0.9995117,"speaker":"A"},{"text":"click","start":1424750,"end":1425110,"confidence":0.9987793,"speaker":"A"},{"text":"the","start":1425110,"end":1425270,"confidence":0.9995117,"speaker":"A"},{"text":"plus","start":1425270,"end":1425550,"confidence":0.9980469,"speaker":"A"},{"text":"sign,","start":1425550,"end":1425870,"confidence":0.9980469,"speaker":"A"},{"text":"you","start":1426840,"end":1427000,"confidence":0.9980469,"speaker":"A"},{"text":"say","start":1427000,"end":1427200,"confidence":0.99853516,"speaker":"A"},{"text":"a","start":1427200,"end":1427320,"confidence":0.91064453,"speaker":"A"},{"text":"name","start":1427320,"end":1427560,"confidence":0.99609375,"speaker":"A"},{"text":"and","start":1428600,"end":1428920,"confidence":0.9975586,"speaker":"A"},{"text":"you","start":1428920,"end":1429120,"confidence":0.99902344,"speaker":"A"},{"text":"say","start":1429120,"end":1429280,"confidence":0.9980469,"speaker":"A"},{"text":"whether","start":1429280,"end":1429440,"confidence":0.9995117,"speaker":"A"},{"text":"you","start":1429440,"end":1429600,"confidence":1,"speaker":"A"},{"text":"want","start":1429600,"end":1429720,"confidence":0.99560547,"speaker":"A"},{"text":"to","start":1429720,"end":1429800,"confidence":0.99560547,"speaker":"A"},{"text":"do","start":1429800,"end":1429920,"confidence":0.99853516,"speaker":"A"},{"text":"a","start":1429920,"end":1430040,"confidence":0.9995117,"speaker":"A"},{"text":"post","start":1430040,"end":1430240,"confidence":0.9995117,"speaker":"A"},{"text":"message","start":1430240,"end":1430680,"confidence":0.99902344,"speaker":"A"},{"text":"or","start":1430680,"end":1430920,"confidence":0.9995117,"speaker":"A"},{"text":"URL","start":1430920,"end":1431440,"confidence":0.8330078,"speaker":"A"},{"text":"redirect.","start":1431440,"end":1432040,"confidence":1,"speaker":"A"},{"text":"We'll","start":1432280,"end":1432640,"confidence":0.9708659,"speaker":"A"},{"text":"get","start":1432640,"end":1432800,"confidence":1,"speaker":"A"},{"text":"into","start":1432800,"end":1432960,"confidence":1,"speaker":"A"},{"text":"that","start":1432960,"end":1433120,"confidence":1,"speaker":"A"},{"text":"in","start":1433120,"end":1433280,"confidence":0.8725586,"speaker":"A"},{"text":"a","start":1433280,"end":1433400,"confidence":0.99902344,"speaker":"A"},{"text":"little","start":1433400,"end":1433560,"confidence":0.9526367,"speaker":"A"},{"text":"bit","start":1433560,"end":1433760,"confidence":1,"speaker":"A"},{"text":"in","start":1433760,"end":1433920,"confidence":0.99609375,"speaker":"A"},{"text":"the","start":1433920,"end":1434040,"confidence":0.9995117,"speaker":"A"},{"text":"next","start":1434040,"end":1434200,"confidence":0.9995117,"speaker":"A"},{"text":"section.","start":1434200,"end":1434680,"confidence":0.9995117,"speaker":"A"},{"text":"And","start":1435960,"end":1436240,"confidence":0.98828125,"speaker":"A"},{"text":"then","start":1436240,"end":1436480,"confidence":0.89453125,"speaker":"A"},{"text":"whether","start":1436480,"end":1436760,"confidence":0.9995117,"speaker":"A"},{"text":"you","start":1436760,"end":1436960,"confidence":1,"speaker":"A"},{"text":"want","start":1436960,"end":1437120,"confidence":0.9975586,"speaker":"A"},{"text":"to","start":1437120,"end":1437280,"confidence":1,"speaker":"A"},{"text":"have","start":1437280,"end":1437560,"confidence":1,"speaker":"A"},{"text":"user","start":1437800,"end":1438280,"confidence":0.99902344,"speaker":"A"},{"text":"info","start":1438280,"end":1438760,"confidence":1,"speaker":"A"},{"text":"and","start":1438840,"end":1439240,"confidence":0.99609375,"speaker":"A"},{"text":"you","start":1439400,"end":1439720,"confidence":0.99609375,"speaker":"A"},{"text":"click","start":1439720,"end":1440040,"confidence":0.9995117,"speaker":"A"},{"text":"save","start":1440040,"end":1440360,"confidence":0.9987793,"speaker":"A"},{"text":"and","start":1440360,"end":1440640,"confidence":0.9326172,"speaker":"A"},{"text":"you'll","start":1440640,"end":1440920,"confidence":0.99934894,"speaker":"A"},{"text":"get","start":1440920,"end":1441040,"confidence":1,"speaker":"A"},{"text":"a","start":1441040,"end":1441160,"confidence":0.9995117,"speaker":"A"},{"text":"nice","start":1441160,"end":1441400,"confidence":0.99975586,"speaker":"A"},{"text":"little","start":1441400,"end":1441680,"confidence":0.9995117,"speaker":"A"},{"text":"API","start":1441680,"end":1442280,"confidence":0.86499023,"speaker":"A"},{"text":"token","start":1442519,"end":1442960,"confidence":0.9996745,"speaker":"A"},{"text":"you","start":1442960,"end":1443120,"confidence":0.9995117,"speaker":"A"},{"text":"could","start":1443120,"end":1443280,"confidence":0.9951172,"speaker":"A"},{"text":"use","start":1443280,"end":1443520,"confidence":1,"speaker":"A"},{"text":"in","start":1443520,"end":1443760,"confidence":0.99658203,"speaker":"A"},{"text":"your","start":1443760,"end":1444040,"confidence":0.9848633,"speaker":"A"},{"text":"web","start":1444120,"end":1444600,"confidence":0.99560547,"speaker":"A"},{"text":"your","start":1445240,"end":1445560,"confidence":0.9873047,"speaker":"A"},{"text":"web","start":1445560,"end":1445840,"confidence":0.9987793,"speaker":"A"},{"text":"calls","start":1445840,"end":1446160,"confidence":0.9831543,"speaker":"A"},{"text":"essentially.","start":1446160,"end":1446680,"confidence":0.9581299,"speaker":"A"},{"text":"API","start":1449000,"end":1449560,"confidence":0.8713379,"speaker":"A"},{"text":"doesn't","start":1449560,"end":1449800,"confidence":0.99886066,"speaker":"A"},{"text":"really.","start":1449800,"end":1450000,"confidence":0.9980469,"speaker":"A"},{"text":"The","start":1450000,"end":1450200,"confidence":0.88720703,"speaker":"A"},{"text":"API","start":1450200,"end":1450640,"confidence":0.954834,"speaker":"A"},{"text":"token","start":1450640,"end":1451000,"confidence":0.99934894,"speaker":"A"},{"text":"doesn't","start":1451000,"end":1451200,"confidence":0.9160156,"speaker":"A"},{"text":"really","start":1451200,"end":1451360,"confidence":0.9995117,"speaker":"A"},{"text":"give","start":1451360,"end":1451520,"confidence":1,"speaker":"A"},{"text":"you","start":1451520,"end":1451680,"confidence":0.99853516,"speaker":"A"},{"text":"a","start":1451680,"end":1451800,"confidence":0.99853516,"speaker":"A"},{"text":"lot","start":1451800,"end":1452040,"confidence":0.99560547,"speaker":"A"},{"text":"of.","start":1452100,"end":1452260,"confidence":0.515625,"speaker":"A"},{"text":"But","start":1452570,"end":1452690,"confidence":0.98535156,"speaker":"A"},{"text":"what","start":1452690,"end":1452850,"confidence":0.99658203,"speaker":"A"},{"text":"it","start":1452850,"end":1452970,"confidence":0.9902344,"speaker":"A"},{"text":"does","start":1452970,"end":1453130,"confidence":0.9980469,"speaker":"A"},{"text":"give","start":1453130,"end":1453290,"confidence":0.99853516,"speaker":"A"},{"text":"you","start":1453290,"end":1453410,"confidence":0.99853516,"speaker":"A"},{"text":"is","start":1453410,"end":1453570,"confidence":0.98779297,"speaker":"A"},{"text":"it","start":1453570,"end":1453690,"confidence":0.9951172,"speaker":"A"},{"text":"gives","start":1453690,"end":1453890,"confidence":0.9733887,"speaker":"A"},{"text":"you","start":1453890,"end":1454010,"confidence":1,"speaker":"A"},{"text":"an","start":1454010,"end":1454170,"confidence":1,"speaker":"A"},{"text":"entry","start":1454170,"end":1454530,"confidence":0.99975586,"speaker":"A"},{"text":"to","start":1454530,"end":1454850,"confidence":1,"speaker":"A"},{"text":"get","start":1454850,"end":1455130,"confidence":0.9995117,"speaker":"A"},{"text":"a","start":1455130,"end":1455330,"confidence":0.9995117,"speaker":"A"},{"text":"web","start":1455330,"end":1455570,"confidence":1,"speaker":"A"},{"text":"authentication","start":1455570,"end":1456250,"confidence":0.8823242,"speaker":"A"},{"text":"token","start":1456250,"end":1456610,"confidence":0.9998372,"speaker":"A"},{"text":"for","start":1456610,"end":1456770,"confidence":0.9995117,"speaker":"A"},{"text":"a","start":1456770,"end":1456930,"confidence":0.48901367,"speaker":"A"},{"text":"user.","start":1456930,"end":1457450,"confidence":0.99902344,"speaker":"A"},{"text":"So","start":1457850,"end":1458130,"confidence":0.99121094,"speaker":"A"},{"text":"basically","start":1458130,"end":1458570,"confidence":0.99853516,"speaker":"A"},{"text":"the","start":1458730,"end":1459010,"confidence":1,"speaker":"A"},{"text":"way","start":1459010,"end":1459210,"confidence":0.9995117,"speaker":"A"},{"text":"that","start":1459210,"end":1459450,"confidence":1,"speaker":"A"},{"text":"works.","start":1459450,"end":1459930,"confidence":0.99731445,"speaker":"A"},{"text":"So","start":1460970,"end":1461370,"confidence":0.9580078,"speaker":"A"},{"text":"you'll","start":1461450,"end":1461810,"confidence":0.93896484,"speaker":"A"},{"text":"notice","start":1461810,"end":1462170,"confidence":0.99975586,"speaker":"A"},{"text":"here,","start":1462170,"end":1462490,"confidence":0.99902344,"speaker":"A"},{"text":"when","start":1463050,"end":1463370,"confidence":0.9941406,"speaker":"A"},{"text":"we","start":1463370,"end":1463570,"confidence":0.9995117,"speaker":"A"},{"text":"were","start":1463570,"end":1463770,"confidence":0.99853516,"speaker":"A"},{"text":"in","start":1463770,"end":1463970,"confidence":1,"speaker":"A"},{"text":"this","start":1463970,"end":1464250,"confidence":0.9995117,"speaker":"A"},{"text":"section,","start":1464330,"end":1464890,"confidence":0.99975586,"speaker":"A"},{"text":"we","start":1467050,"end":1467330,"confidence":0.9980469,"speaker":"A"},{"text":"have","start":1467330,"end":1467490,"confidence":0.9995117,"speaker":"A"},{"text":"this","start":1467490,"end":1467690,"confidence":1,"speaker":"A"},{"text":"piece","start":1467690,"end":1467970,"confidence":0.9998372,"speaker":"A"},{"text":"here","start":1467970,"end":1468250,"confidence":0.99902344,"speaker":"A"},{"text":"called","start":1468250,"end":1468569,"confidence":0.99902344,"speaker":"A"},{"text":"Sign","start":1468569,"end":1468770,"confidence":0.9926758,"speaker":"A"},{"text":"in","start":1468770,"end":1468970,"confidence":0.48339844,"speaker":"A"},{"text":"Callback.","start":1468970,"end":1469610,"confidence":0.9967448,"speaker":"A"},{"text":"So","start":1469770,"end":1470170,"confidence":0.9580078,"speaker":"A"},{"text":"you","start":1470330,"end":1470650,"confidence":0.9995117,"speaker":"A"},{"text":"can","start":1470650,"end":1470930,"confidence":0.9995117,"speaker":"A"},{"text":"have","start":1470930,"end":1471250,"confidence":0.98291016,"speaker":"A"},{"text":"either","start":1471250,"end":1471690,"confidence":1,"speaker":"A"},{"text":"call","start":1471690,"end":1472010,"confidence":0.9741211,"speaker":"A"},{"text":"a","start":1472010,"end":1472210,"confidence":0.96875,"speaker":"A"},{"text":"JavaScript,","start":1472210,"end":1472970,"confidence":0.9967448,"speaker":"A"},{"text":"it's","start":1473370,"end":1473730,"confidence":0.99593097,"speaker":"A"},{"text":"called","start":1473730,"end":1473930,"confidence":0.9995117,"speaker":"A"},{"text":"a","start":1473930,"end":1474130,"confidence":0.9794922,"speaker":"A"},{"text":"message","start":1474130,"end":1474530,"confidence":0.9980469,"speaker":"A"},{"text":"event,","start":1474530,"end":1474810,"confidence":0.9897461,"speaker":"A"},{"text":"it","start":1475610,"end":1475890,"confidence":0.9941406,"speaker":"A"},{"text":"will","start":1475890,"end":1476090,"confidence":0.82177734,"speaker":"A"},{"text":"call","start":1476090,"end":1476330,"confidence":0.6923828,"speaker":"A"},{"text":"a","start":1476330,"end":1476530,"confidence":0.90625,"speaker":"A"},{"text":"Message","start":1476530,"end":1476850,"confidence":0.99902344,"speaker":"A"},{"text":"event","start":1476850,"end":1477090,"confidence":0.9897461,"speaker":"A"},{"text":"and","start":1477090,"end":1477450,"confidence":0.97265625,"speaker":"A"},{"text":"a","start":1477450,"end":1477730,"confidence":0.8847656,"speaker":"A"},{"text":"message","start":1477730,"end":1478050,"confidence":0.9987793,"speaker":"A"},{"text":"event","start":1478050,"end":1478250,"confidence":0.9951172,"speaker":"A"},{"text":"will","start":1478250,"end":1478450,"confidence":0.9921875,"speaker":"A"},{"text":"have","start":1478450,"end":1478610,"confidence":1,"speaker":"A"},{"text":"the","start":1478610,"end":1478730,"confidence":0.9975586,"speaker":"A"},{"text":"metadata","start":1478730,"end":1479250,"confidence":0.99886066,"speaker":"A"},{"text":"with","start":1479250,"end":1479410,"confidence":0.9995117,"speaker":"A"},{"text":"the","start":1479410,"end":1479530,"confidence":0.99560547,"speaker":"A"},{"text":"web","start":1479530,"end":1479730,"confidence":0.9992676,"speaker":"A"},{"text":"authentication","start":1479730,"end":1480410,"confidence":0.99975586,"speaker":"A"},{"text":"token","start":1480410,"end":1480770,"confidence":0.9998372,"speaker":"A"},{"text":"of","start":1480770,"end":1480930,"confidence":0.99902344,"speaker":"A"},{"text":"that","start":1480930,"end":1481090,"confidence":0.99902344,"speaker":"A"},{"text":"user.","start":1481090,"end":1481530,"confidence":0.99902344,"speaker":"A"},{"text":"Or","start":1482410,"end":1482530,"confidence":0.9902344,"speaker":"A"},{"text":"you","start":1482530,"end":1482650,"confidence":0.7363281,"speaker":"A"},{"text":"could","start":1482650,"end":1482770,"confidence":0.99072266,"speaker":"A"},{"text":"do","start":1482770,"end":1482930,"confidence":0.9946289,"speaker":"A"},{"text":"URL","start":1482930,"end":1483450,"confidence":0.99658203,"speaker":"A"},{"text":"redirect","start":1483450,"end":1484090,"confidence":0.99975586,"speaker":"A"},{"text":"where","start":1484170,"end":1484570,"confidence":0.99121094,"speaker":"A"},{"text":"on","start":1484810,"end":1485210,"confidence":0.8457031,"speaker":"A"},{"text":"authentication","start":1485290,"end":1486050,"confidence":0.99975586,"speaker":"A"},{"text":"the","start":1486050,"end":1486290,"confidence":0.9975586,"speaker":"A"},{"text":"user","start":1486290,"end":1486730,"confidence":0.99975586,"speaker":"A"},{"text":"has","start":1486970,"end":1487250,"confidence":0.99902344,"speaker":"A"},{"text":"a","start":1487250,"end":1487410,"confidence":0.9975586,"speaker":"A"},{"text":"URL","start":1487410,"end":1487930,"confidence":0.998291,"speaker":"A"},{"text":"and","start":1487930,"end":1488130,"confidence":0.99609375,"speaker":"A"},{"text":"then","start":1488130,"end":1488290,"confidence":0.9560547,"speaker":"A"},{"text":"part","start":1488290,"end":1488450,"confidence":1,"speaker":"A"},{"text":"of","start":1488450,"end":1488570,"confidence":1,"speaker":"A"},{"text":"that","start":1488570,"end":1488690,"confidence":0.9995117,"speaker":"A"},{"text":"URL","start":1488690,"end":1489170,"confidence":0.9980469,"speaker":"A"},{"text":"is","start":1489170,"end":1489330,"confidence":0.99609375,"speaker":"A"},{"text":"then","start":1489330,"end":1489530,"confidence":0.98291016,"speaker":"A"},{"text":"having","start":1489530,"end":1489850,"confidence":0.99658203,"speaker":"A"},{"text":"part","start":1490650,"end":1490930,"confidence":0.9921875,"speaker":"A"},{"text":"of","start":1490930,"end":1491090,"confidence":0.99853516,"speaker":"A"},{"text":"one","start":1491090,"end":1491210,"confidence":0.9995117,"speaker":"A"},{"text":"of","start":1491210,"end":1491290,"confidence":0.9995117,"speaker":"A"},{"text":"the","start":1491290,"end":1491370,"confidence":1,"speaker":"A"},{"text":"query","start":1491370,"end":1491690,"confidence":0.8486328,"speaker":"A"},{"text":"parameters","start":1491770,"end":1492570,"confidence":0.8824463,"speaker":"A"},{"text":"and","start":1492570,"end":1492850,"confidence":0.9814453,"speaker":"A"},{"text":"we'll","start":1492850,"end":1493050,"confidence":0.99934894,"speaker":"A"},{"text":"get","start":1493050,"end":1493130,"confidence":1,"speaker":"A"},{"text":"into","start":1493130,"end":1493290,"confidence":0.99902344,"speaker":"A"},{"text":"that.","start":1493290,"end":1493610,"confidence":0.9975586,"speaker":"A"},{"text":"We'll","start":1494250,"end":1494570,"confidence":0.89176434,"speaker":"A"},{"text":"then","start":1494570,"end":1494690,"confidence":0.9980469,"speaker":"A"},{"text":"have","start":1494690,"end":1494850,"confidence":1,"speaker":"A"},{"text":"the","start":1494850,"end":1495010,"confidence":0.9980469,"speaker":"A"},{"text":"web","start":1495010,"end":1495250,"confidence":0.9904785,"speaker":"A"},{"text":"authentication","start":1495250,"end":1495810,"confidence":0.9975586,"speaker":"A"},{"text":"token","start":1495810,"end":1496130,"confidence":0.9996745,"speaker":"A"},{"text":"in","start":1496130,"end":1496290,"confidence":0.99560547,"speaker":"A"},{"text":"the","start":1496290,"end":1496450,"confidence":1,"speaker":"A"},{"text":"URL.","start":1496450,"end":1497050,"confidence":0.99731445,"speaker":"A"},{"text":"So","start":1498570,"end":1498970,"confidence":0.9921875,"speaker":"A"},{"text":"you","start":1499050,"end":1499330,"confidence":0.9794922,"speaker":"A"},{"text":"put,","start":1499330,"end":1499610,"confidence":0.9970703,"speaker":"A"},{"text":"basically","start":1500010,"end":1500410,"confidence":0.9995117,"speaker":"A"},{"text":"you","start":1500410,"end":1500570,"confidence":0.71972656,"speaker":"A"},{"text":"have","start":1500570,"end":1500690,"confidence":0.99853516,"speaker":"A"},{"text":"your","start":1500690,"end":1500850,"confidence":1,"speaker":"A"},{"text":"website,","start":1500850,"end":1501130,"confidence":0.9980469,"speaker":"A"},{"text":"you","start":1501450,"end":1501850,"confidence":0.9995117,"speaker":"A"},{"text":"add","start":1501850,"end":1502130,"confidence":0.9980469,"speaker":"A"},{"text":"the","start":1502130,"end":1502290,"confidence":0.9995117,"speaker":"A"},{"text":"JavaScript,","start":1502290,"end":1503050,"confidence":0.9950358,"speaker":"A"},{"text":"you","start":1503210,"end":1503490,"confidence":0.99658203,"speaker":"A"},{"text":"need","start":1503490,"end":1503770,"confidence":0.9995117,"speaker":"A"},{"text":"to","start":1504330,"end":1504730,"confidence":0.99902344,"speaker":"A"},{"text":"add","start":1504970,"end":1505330,"confidence":0.9892578,"speaker":"A"},{"text":"the","start":1505330,"end":1505570,"confidence":0.9975586,"speaker":"A"},{"text":"sign","start":1505570,"end":1505770,"confidence":0.99902344,"speaker":"A"},{"text":"in","start":1505770,"end":1505970,"confidence":0.99609375,"speaker":"A"},{"text":"with","start":1505970,"end":1506170,"confidence":1,"speaker":"A"},{"text":"Apple.","start":1506170,"end":1506650,"confidence":0.9987793,"speaker":"A"},{"text":"Oh,","start":1506970,"end":1507330,"confidence":0.8078613,"speaker":"A"},{"text":"here's","start":1507330,"end":1507650,"confidence":0.9991862,"speaker":"A"},{"text":"Josh.","start":1507650,"end":1508010,"confidence":0.9987793,"speaker":"A"},{"text":"Oh","start":1514310,"end":1514510,"confidence":0.9213867,"speaker":"A"},{"text":"cool.","start":1514510,"end":1514870,"confidence":0.99902344,"speaker":"A"},{"text":"Josh,","start":1514870,"end":1515350,"confidence":0.9995117,"speaker":"A"},{"text":"you","start":1515350,"end":1515590,"confidence":0.97265625,"speaker":"A"},{"text":"there?","start":1515590,"end":1515910,"confidence":0.9995117,"speaker":"A"},{"text":"I","start":1518790,"end":1519110,"confidence":0.99853516,"speaker":"C"},{"text":"hope","start":1519110,"end":1519390,"confidence":1,"speaker":"C"},{"text":"so.","start":1519390,"end":1519750,"confidence":0.99902344,"speaker":"C"},{"text":"Good.","start":1520710,"end":1521070,"confidence":0.9868164,"speaker":"A"},{"text":"Okay.","start":1521070,"end":1521590,"confidence":0.97753906,"speaker":"A"},{"text":"Hey,","start":1521750,"end":1522110,"confidence":0.9992676,"speaker":"A"},{"text":"we","start":1522110,"end":1522230,"confidence":0.99902344,"speaker":"A"},{"text":"were","start":1522230,"end":1522350,"confidence":0.51660156,"speaker":"A"},{"text":"just","start":1522350,"end":1522510,"confidence":1,"speaker":"A"},{"text":"talking","start":1522510,"end":1522750,"confidence":0.99975586,"speaker":"A"},{"text":"about","start":1522750,"end":1522990,"confidence":0.9970703,"speaker":"A"},{"text":"how","start":1522990,"end":1523230,"confidence":0.9995117,"speaker":"A"},{"text":"to","start":1523230,"end":1523430,"confidence":0.9902344,"speaker":"A"},{"text":"set","start":1523430,"end":1523630,"confidence":1,"speaker":"A"},{"text":"up.","start":1523630,"end":1523790,"confidence":0.984375,"speaker":"A"},{"text":"I'm","start":1523790,"end":1523990,"confidence":0.9970703,"speaker":"A"},{"text":"going","start":1523990,"end":1524070,"confidence":0.5854492,"speaker":"A"},{"text":"to","start":1524070,"end":1524150,"confidence":0.9951172,"speaker":"A"},{"text":"go","start":1524150,"end":1524269,"confidence":0.9975586,"speaker":"A"},{"text":"back","start":1524269,"end":1524429,"confidence":0.9995117,"speaker":"A"},{"text":"a","start":1524429,"end":1524550,"confidence":0.99902344,"speaker":"A"},{"text":"little","start":1524550,"end":1524630,"confidence":1,"speaker":"A"},{"text":"bit","start":1524630,"end":1524750,"confidence":0.99853516,"speaker":"A"},{"text":"Evan,","start":1524750,"end":1525190,"confidence":0.86279297,"speaker":"A"},{"text":"but","start":1525510,"end":1525790,"confidence":0.98535156,"speaker":"A"},{"text":"not","start":1525790,"end":1525950,"confidence":0.99316406,"speaker":"A"},{"text":"too","start":1525950,"end":1526110,"confidence":0.9980469,"speaker":"A"},{"text":"far","start":1526110,"end":1526310,"confidence":1,"speaker":"A"},{"text":"back.","start":1526310,"end":1526630,"confidence":0.99853516,"speaker":"A"},{"text":"Yeah,","start":1527110,"end":1527430,"confidence":0.9895833,"speaker":"B"},{"text":"no","start":1527430,"end":1527550,"confidence":0.9824219,"speaker":"B"},{"text":"worries.","start":1527550,"end":1527910,"confidence":0.998291,"speaker":"B"},{"text":"That's","start":1527990,"end":1528310,"confidence":0.99625653,"speaker":"A"},{"text":"okay.","start":1528310,"end":1528710,"confidence":0.9635417,"speaker":"A"},{"text":"But","start":1530470,"end":1530750,"confidence":0.9370117,"speaker":"A"},{"text":"we","start":1530750,"end":1530910,"confidence":0.9995117,"speaker":"A"},{"text":"talked","start":1530910,"end":1531110,"confidence":0.97265625,"speaker":"A"},{"text":"about","start":1531110,"end":1531270,"confidence":0.9980469,"speaker":"A"},{"text":"setting","start":1531270,"end":1531510,"confidence":0.9995117,"speaker":"A"},{"text":"up","start":1531510,"end":1531750,"confidence":0.99902344,"speaker":"A"},{"text":"API","start":1531830,"end":1532390,"confidence":0.9980469,"speaker":"A"},{"text":"token","start":1532390,"end":1532950,"confidence":1,"speaker":"A"},{"text":"and","start":1533270,"end":1533590,"confidence":0.9946289,"speaker":"A"},{"text":"how","start":1533590,"end":1533790,"confidence":1,"speaker":"A"},{"text":"to","start":1533790,"end":1533910,"confidence":0.9995117,"speaker":"A"},{"text":"do","start":1533910,"end":1534030,"confidence":1,"speaker":"A"},{"text":"that.","start":1534030,"end":1534310,"confidence":0.9995117,"speaker":"A"},{"text":"So","start":1535910,"end":1536150,"confidence":0.9707031,"speaker":"A"},{"text":"you","start":1536950,"end":1537350,"confidence":0.9169922,"speaker":"A"},{"text":"go","start":1537430,"end":1537710,"confidence":0.99072266,"speaker":"A"},{"text":"in","start":1537710,"end":1537870,"confidence":0.9941406,"speaker":"A"},{"text":"here,","start":1537870,"end":1538150,"confidence":0.9995117,"speaker":"A"},{"text":"you","start":1538150,"end":1538430,"confidence":0.9819336,"speaker":"A"},{"text":"just","start":1538430,"end":1538550,"confidence":0.9970703,"speaker":"A"},{"text":"click","start":1538550,"end":1538790,"confidence":0.9995117,"speaker":"A"},{"text":"plus,","start":1538790,"end":1539110,"confidence":0.9655762,"speaker":"A"},{"text":"you","start":1539110,"end":1539350,"confidence":0.9897461,"speaker":"A"},{"text":"select","start":1539350,"end":1539630,"confidence":0.9995117,"speaker":"A"},{"text":"your","start":1539630,"end":1539790,"confidence":0.9975586,"speaker":"A"},{"text":"sign","start":1539790,"end":1539990,"confidence":0.99658203,"speaker":"A"},{"text":"in","start":1539990,"end":1540190,"confidence":0.9428711,"speaker":"A"},{"text":"callback","start":1540190,"end":1540710,"confidence":0.9742839,"speaker":"A"},{"text":"and","start":1540710,"end":1540950,"confidence":0.99365234,"speaker":"A"},{"text":"you","start":1540950,"end":1541150,"confidence":0.98828125,"speaker":"A"},{"text":"put","start":1541150,"end":1541310,"confidence":1,"speaker":"A"},{"text":"in","start":1541310,"end":1541470,"confidence":0.9379883,"speaker":"A"},{"text":"a","start":1541470,"end":1541670,"confidence":0.9404297,"speaker":"A"},{"text":"name","start":1541670,"end":1541990,"confidence":0.99902344,"speaker":"A"},{"text":"and","start":1542630,"end":1542910,"confidence":0.90283203,"speaker":"A"},{"text":"it'll","start":1542910,"end":1543150,"confidence":0.84277344,"speaker":"A"},{"text":"give","start":1543150,"end":1543310,"confidence":1,"speaker":"A"},{"text":"you","start":1543310,"end":1543590,"confidence":0.9995117,"speaker":"A"},{"text":"an","start":1543750,"end":1544030,"confidence":0.9770508,"speaker":"A"},{"text":"API","start":1544030,"end":1544470,"confidence":0.8105469,"speaker":"A"},{"text":"token","start":1544470,"end":1544950,"confidence":0.9941406,"speaker":"A"},{"text":"once","start":1544950,"end":1545150,"confidence":0.99902344,"speaker":"A"},{"text":"you","start":1545150,"end":1545310,"confidence":0.9995117,"speaker":"A"},{"text":"click","start":1545310,"end":1545550,"confidence":0.99975586,"speaker":"A"},{"text":"save.","start":1545550,"end":1545830,"confidence":0.9980469,"speaker":"A"},{"text":"Basically.","start":1545830,"end":1546310,"confidence":0.9953613,"speaker":"A"},{"text":"Come","start":1550549,"end":1550870,"confidence":0.9658203,"speaker":"A"},{"text":"on.","start":1550870,"end":1551190,"confidence":0.99853516,"speaker":"A"},{"text":"The","start":1554470,"end":1554710,"confidence":0.9975586,"speaker":"A"},{"text":"reason","start":1554710,"end":1554910,"confidence":1,"speaker":"A"},{"text":"you","start":1554910,"end":1555150,"confidence":0.84814453,"speaker":"A"},{"text":"want","start":1555150,"end":1555310,"confidence":0.99902344,"speaker":"A"},{"text":"an","start":1555310,"end":1555470,"confidence":0.99658203,"speaker":"A"},{"text":"API","start":1555470,"end":1555830,"confidence":0.79589844,"speaker":"A"},{"text":"token","start":1555830,"end":1556190,"confidence":0.9998372,"speaker":"A"},{"text":"is","start":1556190,"end":1556390,"confidence":0.9941406,"speaker":"A"},{"text":"this","start":1556390,"end":1556590,"confidence":0.99902344,"speaker":"A"},{"text":"allows","start":1556590,"end":1556990,"confidence":0.99975586,"speaker":"A"},{"text":"you","start":1556990,"end":1557190,"confidence":0.9995117,"speaker":"A"},{"text":"to","start":1557190,"end":1557390,"confidence":0.9946289,"speaker":"A"},{"text":"then","start":1557390,"end":1557670,"confidence":0.95654297,"speaker":"A"},{"text":"have","start":1558550,"end":1558830,"confidence":0.9995117,"speaker":"A"},{"text":"users","start":1558830,"end":1559350,"confidence":0.99886066,"speaker":"A"},{"text":"Sign","start":1559350,"end":1559670,"confidence":1,"speaker":"A"},{"text":"in","start":1559670,"end":1559990,"confidence":0.9448242,"speaker":"A"},{"text":"to","start":1559990,"end":1560390,"confidence":0.9980469,"speaker":"A"},{"text":"CloudKit","start":1560390,"end":1561190,"confidence":0.97046,"speaker":"A"},{"text":"either","start":1562820,"end":1563060,"confidence":0.99902344,"speaker":"A"},{"text":"using,","start":1563060,"end":1563380,"confidence":0.9873047,"speaker":"A"},{"text":"using","start":1565140,"end":1565500,"confidence":1,"speaker":"A"},{"text":"the","start":1565500,"end":1565860,"confidence":0.9794922,"speaker":"A"},{"text":"the","start":1566420,"end":1566700,"confidence":0.99853516,"speaker":"A"},{"text":"web","start":1566700,"end":1567060,"confidence":0.99975586,"speaker":"A"},{"text":"service","start":1567140,"end":1567540,"confidence":0.9995117,"speaker":"A"},{"text":"like","start":1567620,"end":1567940,"confidence":0.9995117,"speaker":"A"},{"text":"Curl","start":1567940,"end":1568580,"confidence":0.8334961,"speaker":"A"},{"text":"or","start":1568900,"end":1569300,"confidence":1,"speaker":"A"},{"text":"you","start":1569300,"end":1569580,"confidence":0.9995117,"speaker":"A"},{"text":"could","start":1569580,"end":1569820,"confidence":0.99609375,"speaker":"A"},{"text":"also","start":1569820,"end":1570140,"confidence":1,"speaker":"A"},{"text":"do","start":1570140,"end":1570380,"confidence":1,"speaker":"A"},{"text":"it","start":1570380,"end":1570540,"confidence":1,"speaker":"A"},{"text":"through","start":1570540,"end":1570700,"confidence":1,"speaker":"A"},{"text":"a","start":1570700,"end":1570860,"confidence":1,"speaker":"A"},{"text":"website","start":1570860,"end":1571100,"confidence":0.9995117,"speaker":"A"},{"text":"using","start":1571100,"end":1571380,"confidence":0.9995117,"speaker":"A"},{"text":"CloudKit","start":1571380,"end":1571980,"confidence":0.998291,"speaker":"A"},{"text":"js.","start":1571980,"end":1572500,"confidence":0.83740234,"speaker":"A"},{"text":"So","start":1573780,"end":1574180,"confidence":0.99560547,"speaker":"A"},{"text":"web","start":1574420,"end":1574820,"confidence":0.97021484,"speaker":"A"},{"text":"authentication","start":1574820,"end":1575500,"confidence":0.9995117,"speaker":"A"},{"text":"token","start":1575500,"end":1576100,"confidence":0.9991862,"speaker":"A"},{"text":"we","start":1576100,"end":1576420,"confidence":0.9995117,"speaker":"A"},{"text":"talked","start":1576420,"end":1576700,"confidence":0.99975586,"speaker":"A"},{"text":"about","start":1576700,"end":1576900,"confidence":0.99902344,"speaker":"A"},{"text":"how","start":1576900,"end":1577219,"confidence":0.9995117,"speaker":"A"},{"text":"you","start":1577219,"end":1577460,"confidence":1,"speaker":"A"},{"text":"can","start":1577460,"end":1577539,"confidence":1,"speaker":"A"},{"text":"either","start":1577539,"end":1577740,"confidence":1,"speaker":"A"},{"text":"do","start":1577740,"end":1577900,"confidence":1,"speaker":"A"},{"text":"the","start":1577900,"end":1578060,"confidence":1,"speaker":"A"},{"text":"post","start":1578060,"end":1578300,"confidence":1,"speaker":"A"},{"text":"message","start":1578300,"end":1578780,"confidence":0.9995117,"speaker":"A"},{"text":"or","start":1578780,"end":1578980,"confidence":0.8930664,"speaker":"A"},{"text":"you","start":1578980,"end":1579140,"confidence":0.99902344,"speaker":"A"},{"text":"can","start":1579140,"end":1579260,"confidence":0.99853516,"speaker":"A"},{"text":"do","start":1579260,"end":1579380,"confidence":1,"speaker":"A"},{"text":"the","start":1579380,"end":1579500,"confidence":0.99853516,"speaker":"A"},{"text":"URL","start":1579500,"end":1579860,"confidence":0.77905273,"speaker":"A"},{"text":"redirect.","start":1579860,"end":1580420,"confidence":0.99975586,"speaker":"A"},{"text":"Basically","start":1581140,"end":1581700,"confidence":0.99975586,"speaker":"A"},{"text":"you","start":1581700,"end":1582100,"confidence":1,"speaker":"A"},{"text":"have","start":1582100,"end":1582380,"confidence":1,"speaker":"A"},{"text":"the","start":1582380,"end":1582540,"confidence":0.99121094,"speaker":"A"},{"text":"JavaScript","start":1582540,"end":1583020,"confidence":0.9979655,"speaker":"A"},{"text":"on","start":1583020,"end":1583180,"confidence":1,"speaker":"A"},{"text":"your","start":1583180,"end":1583380,"confidence":1,"speaker":"A"},{"text":"website","start":1583380,"end":1583700,"confidence":0.9951172,"speaker":"A"},{"text":"and","start":1584820,"end":1585180,"confidence":0.9980469,"speaker":"A"},{"text":"there","start":1585180,"end":1585420,"confidence":0.58447266,"speaker":"A"},{"text":"has","start":1585420,"end":1585580,"confidence":0.8017578,"speaker":"A"},{"text":"a","start":1585580,"end":1585700,"confidence":1,"speaker":"A"},{"text":"button,","start":1585700,"end":1585980,"confidence":0.998291,"speaker":"A"},{"text":"click","start":1585980,"end":1586260,"confidence":0.99975586,"speaker":"A"},{"text":"the","start":1586260,"end":1586380,"confidence":0.9995117,"speaker":"A"},{"text":"button,","start":1586380,"end":1586620,"confidence":0.99975586,"speaker":"A"},{"text":"you","start":1586620,"end":1586740,"confidence":0.99853516,"speaker":"A"},{"text":"get","start":1586740,"end":1586860,"confidence":0.99560547,"speaker":"A"},{"text":"this","start":1586860,"end":1587020,"confidence":0.9995117,"speaker":"A"},{"text":"nice","start":1587020,"end":1587260,"confidence":0.99975586,"speaker":"A"},{"text":"little","start":1587260,"end":1587460,"confidence":0.9995117,"speaker":"A"},{"text":"window","start":1587460,"end":1587820,"confidence":0.99975586,"speaker":"A"},{"text":"here","start":1587820,"end":1588100,"confidence":0.9951172,"speaker":"A"},{"text":"sign","start":1588780,"end":1588940,"confidence":0.95947266,"speaker":"A"},{"text":"in","start":1588940,"end":1589260,"confidence":0.99072266,"speaker":"A"},{"text":"and","start":1590860,"end":1591140,"confidence":0.9550781,"speaker":"A"},{"text":"then","start":1591140,"end":1591420,"confidence":0.9970703,"speaker":"A"},{"text":"when","start":1591820,"end":1592100,"confidence":1,"speaker":"A"},{"text":"you","start":1592100,"end":1592300,"confidence":0.9995117,"speaker":"A"},{"text":"sign","start":1592300,"end":1592540,"confidence":0.9995117,"speaker":"A"},{"text":"in","start":1592540,"end":1592820,"confidence":0.98583984,"speaker":"A"},{"text":"if","start":1592820,"end":1593060,"confidence":0.9980469,"speaker":"A"},{"text":"you","start":1593060,"end":1593340,"confidence":0.9995117,"speaker":"A"},{"text":"had","start":1593340,"end":1593660,"confidence":0.9121094,"speaker":"A"},{"text":"selected","start":1593660,"end":1594060,"confidence":0.9992676,"speaker":"A"},{"text":"post","start":1594060,"end":1594380,"confidence":0.9975586,"speaker":"A"},{"text":"message,","start":1594380,"end":1595020,"confidence":0.984375,"speaker":"A"},{"text":"you'll","start":1595340,"end":1595700,"confidence":0.9923503,"speaker":"A"},{"text":"get","start":1595700,"end":1595860,"confidence":0.9995117,"speaker":"A"},{"text":"the","start":1595860,"end":1596020,"confidence":0.9995117,"speaker":"A"},{"text":"web","start":1596020,"end":1596260,"confidence":0.9992676,"speaker":"A"},{"text":"authentication","start":1596260,"end":1597020,"confidence":0.96813965,"speaker":"A"},{"text":"token","start":1597020,"end":1597540,"confidence":0.9998372,"speaker":"A"},{"text":"and","start":1597540,"end":1597820,"confidence":0.5283203,"speaker":"A"},{"text":"the","start":1597820,"end":1598020,"confidence":0.9995117,"speaker":"A"},{"text":"data","start":1598020,"end":1598260,"confidence":0.9995117,"speaker":"A"},{"text":"of","start":1598260,"end":1598500,"confidence":0.9995117,"speaker":"A"},{"text":"the","start":1598500,"end":1598660,"confidence":0.9995117,"speaker":"A"},{"text":"event","start":1598660,"end":1598940,"confidence":0.99853516,"speaker":"A"},{"text":"in","start":1598940,"end":1599260,"confidence":0.9291992,"speaker":"A"},{"text":"JavaScript","start":1599260,"end":1600060,"confidence":0.99348956,"speaker":"A"},{"text":"or","start":1600540,"end":1600900,"confidence":0.99902344,"speaker":"A"},{"text":"you","start":1600900,"end":1601140,"confidence":0.9995117,"speaker":"A"},{"text":"will","start":1601140,"end":1601300,"confidence":0.87109375,"speaker":"A"},{"text":"get","start":1601300,"end":1601460,"confidence":1,"speaker":"A"},{"text":"the","start":1601460,"end":1601580,"confidence":0.9995117,"speaker":"A"},{"text":"web","start":1601580,"end":1601780,"confidence":0.9980469,"speaker":"A"},{"text":"authentication","start":1601780,"end":1602460,"confidence":0.8979492,"speaker":"A"},{"text":"token","start":1602460,"end":1602860,"confidence":0.9996745,"speaker":"A"},{"text":"as","start":1602860,"end":1603060,"confidence":0.9995117,"speaker":"A"},{"text":"a","start":1603060,"end":1603220,"confidence":0.98779297,"speaker":"A"},{"text":"URL","start":1603220,"end":1603820,"confidence":0.86157227,"speaker":"A"},{"text":"in","start":1604300,"end":1604579,"confidence":0.99853516,"speaker":"A"},{"text":"the","start":1604579,"end":1604739,"confidence":1,"speaker":"A"},{"text":"callback","start":1604739,"end":1605260,"confidence":0.9983724,"speaker":"A"},{"text":"URL","start":1605260,"end":1605780,"confidence":0.8745117,"speaker":"A"},{"text":"here.","start":1605780,"end":1606140,"confidence":0.9975586,"speaker":"A"},{"text":"Does","start":1606780,"end":1607060,"confidence":0.9995117,"speaker":"A"},{"text":"that","start":1607060,"end":1607220,"confidence":0.9995117,"speaker":"A"},{"text":"make","start":1607220,"end":1607420,"confidence":0.9926758,"speaker":"A"},{"text":"sense?","start":1607420,"end":1607820,"confidence":0.9995117,"speaker":"A"},{"text":"Yep.","start":1610860,"end":1611420,"confidence":0.7561035,"speaker":"B"},{"text":"Yeah.","start":1612220,"end":1612860,"confidence":0.94124347,"speaker":"A"},{"text":"In","start":1613420,"end":1613740,"confidence":0.9975586,"speaker":"A"},{"text":"some","start":1613740,"end":1613940,"confidence":1,"speaker":"A"},{"text":"cases","start":1613940,"end":1614220,"confidence":0.9995117,"speaker":"A"},{"text":"if","start":1614380,"end":1614660,"confidence":0.99853516,"speaker":"A"},{"text":"you","start":1614660,"end":1614940,"confidence":1,"speaker":"A"},{"text":"scour","start":1615180,"end":1615620,"confidence":0.9921875,"speaker":"A"},{"text":"the","start":1615620,"end":1615860,"confidence":0.9995117,"speaker":"A"},{"text":"Internet","start":1615860,"end":1616295,"confidence":0.99780273,"speaker":"A"},{"text":"so","start":1616295,"end":1616450,"confidence":0.37280273,"speaker":"A"},{"text":"Stack","start":1616520,"end":1616720,"confidence":0.94799805,"speaker":"A"},{"text":"overflow","start":1616720,"end":1617120,"confidence":0.9749756,"speaker":"A"},{"text":"will","start":1617120,"end":1617280,"confidence":0.9916992,"speaker":"A"},{"text":"tell","start":1617280,"end":1617440,"confidence":1,"speaker":"A"},{"text":"you","start":1617440,"end":1617600,"confidence":0.9995117,"speaker":"A"},{"text":"and","start":1617600,"end":1617800,"confidence":0.99658203,"speaker":"A"},{"text":"this","start":1617800,"end":1618000,"confidence":0.99902344,"speaker":"A"},{"text":"has","start":1618000,"end":1618200,"confidence":0.9765625,"speaker":"A"},{"text":"happened","start":1618200,"end":1618520,"confidence":0.99975586,"speaker":"A"},{"text":"to","start":1618520,"end":1618640,"confidence":0.9995117,"speaker":"A"},{"text":"me","start":1618640,"end":1618920,"confidence":0.9995117,"speaker":"A"},{"text":"sometimes","start":1619240,"end":1619720,"confidence":0.9998372,"speaker":"A"},{"text":"it","start":1619720,"end":1619800,"confidence":0.99902344,"speaker":"A"},{"text":"will","start":1619800,"end":1619920,"confidence":0.99853516,"speaker":"A"},{"text":"not","start":1619920,"end":1620080,"confidence":0.9995117,"speaker":"A"},{"text":"be","start":1620080,"end":1620360,"confidence":0.99902344,"speaker":"A"},{"text":"CK","start":1620360,"end":1620920,"confidence":0.89404297,"speaker":"A"},{"text":"web","start":1620920,"end":1621200,"confidence":0.9916992,"speaker":"A"},{"text":"authentication","start":1621200,"end":1621880,"confidence":0.9996338,"speaker":"A"},{"text":"token,","start":1621880,"end":1622360,"confidence":0.9995117,"speaker":"A"},{"text":"sometimes","start":1622360,"end":1622760,"confidence":0.9954427,"speaker":"A"},{"text":"it'll","start":1622760,"end":1623000,"confidence":0.8121745,"speaker":"A"},{"text":"be","start":1623000,"end":1623080,"confidence":0.9995117,"speaker":"A"},{"text":"CK","start":1623080,"end":1623480,"confidence":0.8876953,"speaker":"A"},{"text":"session","start":1623480,"end":1624040,"confidence":0.99902344,"speaker":"A"},{"text":"because","start":1624360,"end":1624760,"confidence":0.99853516,"speaker":"A"},{"text":"that's","start":1625240,"end":1625600,"confidence":0.9996745,"speaker":"A"},{"text":"what","start":1625600,"end":1625760,"confidence":0.99560547,"speaker":"A"},{"text":"Apple","start":1625760,"end":1626040,"confidence":0.99560547,"speaker":"A"},{"text":"likes","start":1626040,"end":1626280,"confidence":0.98999023,"speaker":"A"},{"text":"to","start":1626280,"end":1626360,"confidence":0.9995117,"speaker":"A"},{"text":"do.","start":1626360,"end":1626600,"confidence":0.9995117,"speaker":"A"},{"text":"But","start":1629080,"end":1629360,"confidence":0.99316406,"speaker":"A"},{"text":"it's","start":1629360,"end":1629560,"confidence":0.9995117,"speaker":"A"},{"text":"the","start":1629560,"end":1629680,"confidence":1,"speaker":"A"},{"text":"same","start":1629680,"end":1629840,"confidence":1,"speaker":"A"},{"text":"thing.","start":1629840,"end":1630120,"confidence":0.9995117,"speaker":"A"},{"text":"So","start":1630200,"end":1630480,"confidence":0.99853516,"speaker":"A"},{"text":"you","start":1630480,"end":1630640,"confidence":0.9980469,"speaker":"A"},{"text":"basically","start":1630640,"end":1630920,"confidence":0.99975586,"speaker":"A"},{"text":"want","start":1630920,"end":1631120,"confidence":0.8725586,"speaker":"A"},{"text":"to","start":1631120,"end":1631240,"confidence":1,"speaker":"A"},{"text":"look","start":1631240,"end":1631320,"confidence":1,"speaker":"A"},{"text":"for","start":1631320,"end":1631440,"confidence":1,"speaker":"A"},{"text":"either","start":1631440,"end":1631720,"confidence":0.99975586,"speaker":"A"},{"text":"property","start":1631720,"end":1632200,"confidence":0.99902344,"speaker":"A"},{"text":"or","start":1632200,"end":1632520,"confidence":0.9995117,"speaker":"A"},{"text":"query","start":1632680,"end":1633160,"confidence":0.97436523,"speaker":"A"},{"text":"parameter","start":1633240,"end":1633840,"confidence":0.9998372,"speaker":"A"},{"text":"name","start":1633840,"end":1634160,"confidence":0.99853516,"speaker":"A"},{"text":"and","start":1634160,"end":1634400,"confidence":0.99853516,"speaker":"A"},{"text":"you","start":1634400,"end":1634560,"confidence":0.9980469,"speaker":"A"},{"text":"should","start":1634560,"end":1634720,"confidence":1,"speaker":"A"},{"text":"be","start":1634720,"end":1634880,"confidence":1,"speaker":"A"},{"text":"good","start":1634880,"end":1635040,"confidence":0.9995117,"speaker":"A"},{"text":"to","start":1635040,"end":1635200,"confidence":0.9980469,"speaker":"A"},{"text":"go","start":1635200,"end":1635480,"confidence":0.9975586,"speaker":"A"},{"text":"and","start":1636360,"end":1636640,"confidence":0.99560547,"speaker":"A"},{"text":"then","start":1636640,"end":1636760,"confidence":1,"speaker":"A"},{"text":"you'll","start":1636760,"end":1636960,"confidence":0.9902344,"speaker":"A"},{"text":"have","start":1636960,"end":1637080,"confidence":0.9995117,"speaker":"A"},{"text":"that","start":1637080,"end":1637160,"confidence":0.99902344,"speaker":"A"},{"text":"user","start":1637160,"end":1637400,"confidence":0.99902344,"speaker":"A"},{"text":"as","start":1637400,"end":1637520,"confidence":0.4970703,"speaker":"A"},{"text":"well","start":1637520,"end":1637800,"confidence":0.99316406,"speaker":"A"},{"text":"authentication","start":1637800,"end":1638520,"confidence":0.99902344,"speaker":"A"},{"text":"token","start":1638520,"end":1639080,"confidence":0.9995117,"speaker":"A"},{"text":"you","start":1639960,"end":1640240,"confidence":0.98876953,"speaker":"A"},{"text":"could","start":1640240,"end":1640400,"confidence":0.9658203,"speaker":"A"},{"text":"do.","start":1640400,"end":1640680,"confidence":0.9926758,"speaker":"A"},{"text":"What","start":1640920,"end":1641240,"confidence":0.9736328,"speaker":"A"},{"text":"I,","start":1641240,"end":1641560,"confidence":0.9926758,"speaker":"A"},{"text":"what","start":1641720,"end":1642000,"confidence":0.9086914,"speaker":"A"},{"text":"I've","start":1642000,"end":1642200,"confidence":0.99527997,"speaker":"A"},{"text":"been","start":1642200,"end":1642360,"confidence":0.9995117,"speaker":"A"},{"text":"doing","start":1642360,"end":1642680,"confidence":0.9995117,"speaker":"A"},{"text":"is,","start":1643490,"end":1643730,"confidence":0.9863281,"speaker":"A"},{"text":"is","start":1645170,"end":1645490,"confidence":0.94628906,"speaker":"A"},{"text":"I've","start":1645490,"end":1645850,"confidence":0.9996745,"speaker":"A"},{"text":"been","start":1645850,"end":1646130,"confidence":0.99853516,"speaker":"A"},{"text":"take","start":1647330,"end":1647730,"confidence":0.9165039,"speaker":"A"},{"text":"like","start":1647730,"end":1648050,"confidence":0.99902344,"speaker":"A"},{"text":"making","start":1648050,"end":1648290,"confidence":0.99902344,"speaker":"A"},{"text":"a","start":1648290,"end":1648490,"confidence":0.9995117,"speaker":"A"},{"text":"call","start":1648490,"end":1648690,"confidence":1,"speaker":"A"},{"text":"to","start":1648690,"end":1648930,"confidence":0.9995117,"speaker":"A"},{"text":"a","start":1648930,"end":1649130,"confidence":0.7597656,"speaker":"A"},{"text":"like","start":1649130,"end":1649370,"confidence":0.98779297,"speaker":"A"},{"text":"local","start":1649370,"end":1649690,"confidence":0.9995117,"speaker":"A"},{"text":"server","start":1649690,"end":1650170,"confidence":0.99975586,"speaker":"A"},{"text":"for","start":1650170,"end":1650330,"confidence":0.9995117,"speaker":"A"},{"text":"instance","start":1650330,"end":1650770,"confidence":0.99975586,"speaker":"A"},{"text":"and","start":1651330,"end":1651650,"confidence":0.99853516,"speaker":"A"},{"text":"then","start":1651650,"end":1651970,"confidence":0.99902344,"speaker":"A"},{"text":"essentially","start":1651970,"end":1652690,"confidence":0.9987793,"speaker":"A"},{"text":"then","start":1653410,"end":1653690,"confidence":0.8886719,"speaker":"A"},{"text":"I","start":1653690,"end":1653810,"confidence":1,"speaker":"A"},{"text":"could","start":1653810,"end":1653930,"confidence":0.6508789,"speaker":"A"},{"text":"do","start":1653930,"end":1654090,"confidence":0.9995117,"speaker":"A"},{"text":"whatever","start":1654090,"end":1654330,"confidence":1,"speaker":"A"},{"text":"I","start":1654330,"end":1654490,"confidence":0.9995117,"speaker":"A"},{"text":"want","start":1654490,"end":1654690,"confidence":0.9995117,"speaker":"A"},{"text":"with","start":1654690,"end":1654890,"confidence":0.99853516,"speaker":"A"},{"text":"that","start":1654890,"end":1655050,"confidence":0.9995117,"speaker":"A"},{"text":"web","start":1655050,"end":1655290,"confidence":0.9897461,"speaker":"A"},{"text":"authentication","start":1655290,"end":1655970,"confidence":0.9991455,"speaker":"A"},{"text":"token.","start":1655970,"end":1656330,"confidence":0.9996745,"speaker":"A"},{"text":"As","start":1656330,"end":1656490,"confidence":0.9995117,"speaker":"A"},{"text":"long","start":1656490,"end":1656610,"confidence":1,"speaker":"A"},{"text":"as","start":1656610,"end":1656690,"confidence":0.99853516,"speaker":"A"},{"text":"you","start":1656690,"end":1656770,"confidence":1,"speaker":"A"},{"text":"have","start":1656770,"end":1656890,"confidence":0.9995117,"speaker":"A"},{"text":"the","start":1656890,"end":1657010,"confidence":0.9995117,"speaker":"A"},{"text":"web","start":1657010,"end":1657210,"confidence":0.998291,"speaker":"A"},{"text":"authentication","start":1657210,"end":1657730,"confidence":0.99975586,"speaker":"A"},{"text":"token","start":1657730,"end":1658090,"confidence":0.99902344,"speaker":"A"},{"text":"and","start":1658090,"end":1658210,"confidence":0.9355469,"speaker":"A"},{"text":"the","start":1658210,"end":1658330,"confidence":0.99853516,"speaker":"A"},{"text":"API","start":1658330,"end":1658770,"confidence":0.9987793,"speaker":"A"},{"text":"token","start":1658770,"end":1659329,"confidence":0.9996745,"speaker":"A"},{"text":"you","start":1659570,"end":1659850,"confidence":0.9995117,"speaker":"A"},{"text":"can","start":1659850,"end":1660010,"confidence":0.9995117,"speaker":"A"},{"text":"do","start":1660010,"end":1660170,"confidence":1,"speaker":"A"},{"text":"anything","start":1660170,"end":1660570,"confidence":0.99975586,"speaker":"A"},{"text":"on","start":1660570,"end":1660730,"confidence":0.9995117,"speaker":"A"},{"text":"a","start":1660730,"end":1660850,"confidence":0.99902344,"speaker":"A"},{"text":"private","start":1660850,"end":1661050,"confidence":1,"speaker":"A"},{"text":"database","start":1661050,"end":1661810,"confidence":0.99934894,"speaker":"A"},{"text":"that","start":1662530,"end":1662810,"confidence":0.9975586,"speaker":"A"},{"text":"the","start":1662810,"end":1662930,"confidence":0.9995117,"speaker":"A"},{"text":"user","start":1662930,"end":1663210,"confidence":1,"speaker":"A"},{"text":"has","start":1663210,"end":1663410,"confidence":0.99902344,"speaker":"A"},{"text":"rights","start":1663410,"end":1663690,"confidence":0.9975586,"speaker":"A"},{"text":"to.","start":1663690,"end":1664050,"confidence":0.9824219,"speaker":"A"},{"text":"So","start":1664450,"end":1664850,"confidence":0.9941406,"speaker":"A"},{"text":"you","start":1665890,"end":1666170,"confidence":0.98876953,"speaker":"A"},{"text":"can","start":1666170,"end":1666330,"confidence":0.95703125,"speaker":"A"},{"text":"go,","start":1666330,"end":1666570,"confidence":0.99853516,"speaker":"A"},{"text":"you","start":1666570,"end":1666810,"confidence":0.99560547,"speaker":"A"},{"text":"can","start":1666810,"end":1666970,"confidence":0.5966797,"speaker":"A"},{"text":"go","start":1666970,"end":1667130,"confidence":1,"speaker":"A"},{"text":"to","start":1667130,"end":1667250,"confidence":0.9980469,"speaker":"A"},{"text":"town","start":1667250,"end":1667410,"confidence":0.99902344,"speaker":"A"},{"text":"with","start":1667410,"end":1667610,"confidence":0.99609375,"speaker":"A"},{"text":"that","start":1667610,"end":1667890,"confidence":0.9848633,"speaker":"A"},{"text":"all","start":1669420,"end":1669540,"confidence":0.99365234,"speaker":"A"},{"text":"this","start":1669540,"end":1669700,"confidence":0.8154297,"speaker":"A"},{"text":"stuff","start":1669700,"end":1669900,"confidence":1,"speaker":"A"},{"text":"gets","start":1669900,"end":1670060,"confidence":0.99487305,"speaker":"A"},{"text":"Swift","start":1670060,"end":1670260,"confidence":0.99975586,"speaker":"A"},{"text":"in","start":1670260,"end":1670420,"confidence":0.99902344,"speaker":"A"},{"text":"a","start":1670420,"end":1670540,"confidence":0.9995117,"speaker":"A"},{"text":"cookie","start":1670540,"end":1671020,"confidence":1,"speaker":"A"},{"text":"too.","start":1671020,"end":1671420,"confidence":0.9838867,"speaker":"A"},{"text":"So","start":1671580,"end":1671820,"confidence":0.99658203,"speaker":"A"},{"text":"that","start":1671820,"end":1671940,"confidence":1,"speaker":"A"},{"text":"way","start":1671940,"end":1672180,"confidence":0.9995117,"speaker":"A"},{"text":"it'll","start":1672180,"end":1672540,"confidence":0.8470052,"speaker":"A"},{"text":"work.","start":1672540,"end":1672860,"confidence":1,"speaker":"A"},{"text":"When","start":1673740,"end":1674020,"confidence":1,"speaker":"A"},{"text":"you","start":1674020,"end":1674220,"confidence":0.9995117,"speaker":"A"},{"text":"go","start":1674220,"end":1674460,"confidence":1,"speaker":"A"},{"text":"back,","start":1674460,"end":1674700,"confidence":1,"speaker":"A"},{"text":"if","start":1674700,"end":1674940,"confidence":0.53125,"speaker":"A"},{"text":"you","start":1674940,"end":1675260,"confidence":0.9995117,"speaker":"A"},{"text":"have","start":1675500,"end":1675900,"confidence":0.9995117,"speaker":"A"},{"text":"checked","start":1675900,"end":1676420,"confidence":0.99560547,"speaker":"A"},{"text":"the","start":1676420,"end":1676580,"confidence":1,"speaker":"A"},{"text":"box","start":1676580,"end":1676900,"confidence":0.9995117,"speaker":"A"},{"text":"for","start":1676900,"end":1677180,"confidence":0.99902344,"speaker":"A"},{"text":"allow,","start":1677180,"end":1677500,"confidence":0.99560547,"speaker":"A"},{"text":"it's","start":1678780,"end":1679100,"confidence":0.9899089,"speaker":"A"},{"text":"either","start":1679100,"end":1679340,"confidence":0.99975586,"speaker":"A"},{"text":"a","start":1679340,"end":1679540,"confidence":0.9995117,"speaker":"A"},{"text":"box","start":1679540,"end":1679780,"confidence":0.99975586,"speaker":"A"},{"text":"or","start":1679780,"end":1679980,"confidence":0.99902344,"speaker":"A"},{"text":"JavaScript","start":1679980,"end":1680580,"confidence":0.99934894,"speaker":"A"},{"text":"method","start":1680580,"end":1680900,"confidence":0.99348956,"speaker":"A"},{"text":"property","start":1680900,"end":1681260,"confidence":0.9995117,"speaker":"A"},{"text":"that","start":1681260,"end":1681460,"confidence":0.9995117,"speaker":"A"},{"text":"will","start":1681460,"end":1681700,"confidence":0.9013672,"speaker":"A"},{"text":"say,","start":1681700,"end":1681940,"confidence":0.9975586,"speaker":"A"},{"text":"hey,","start":1681940,"end":1682180,"confidence":0.9992676,"speaker":"A"},{"text":"I","start":1682180,"end":1682300,"confidence":1,"speaker":"A"},{"text":"want","start":1682300,"end":1682420,"confidence":1,"speaker":"A"},{"text":"this","start":1682420,"end":1682580,"confidence":0.99902344,"speaker":"A"},{"text":"to","start":1682580,"end":1682740,"confidence":1,"speaker":"A"},{"text":"persist.","start":1682740,"end":1683260,"confidence":0.9992676,"speaker":"A"},{"text":"It'll","start":1683420,"end":1683780,"confidence":0.9715169,"speaker":"A"},{"text":"be","start":1683780,"end":1683900,"confidence":1,"speaker":"A"},{"text":"Swift","start":1683900,"end":1684100,"confidence":0.9980469,"speaker":"A"},{"text":"in","start":1684100,"end":1684260,"confidence":0.9121094,"speaker":"A"},{"text":"a,","start":1684260,"end":1684420,"confidence":0.7871094,"speaker":"A"},{"text":"in","start":1684420,"end":1684580,"confidence":0.71191406,"speaker":"A"},{"text":"a","start":1684580,"end":1684740,"confidence":0.9995117,"speaker":"A"},{"text":"cookie","start":1684740,"end":1685020,"confidence":0.99975586,"speaker":"A"},{"text":"as","start":1685020,"end":1685179,"confidence":1,"speaker":"A"},{"text":"well.","start":1685179,"end":1685460,"confidence":1,"speaker":"A"},{"text":"So","start":1685460,"end":1685700,"confidence":0.99658203,"speaker":"A"},{"text":"if","start":1685700,"end":1685820,"confidence":1,"speaker":"A"},{"text":"you","start":1685820,"end":1685940,"confidence":1,"speaker":"A"},{"text":"want","start":1685940,"end":1686060,"confidence":0.95751953,"speaker":"A"},{"text":"to","start":1686060,"end":1686220,"confidence":0.97314453,"speaker":"A"},{"text":"spelunk","start":1686220,"end":1686820,"confidence":0.9758301,"speaker":"A"},{"text":"your","start":1686820,"end":1686980,"confidence":0.99560547,"speaker":"A"},{"text":"cookies,","start":1686980,"end":1687260,"confidence":1,"speaker":"A"},{"text":"you","start":1687340,"end":1687580,"confidence":0.99853516,"speaker":"A"},{"text":"can","start":1687580,"end":1687820,"confidence":0.9995117,"speaker":"A"},{"text":"see","start":1687980,"end":1688300,"confidence":0.78027344,"speaker":"A"},{"text":"the","start":1688300,"end":1688500,"confidence":0.9995117,"speaker":"A"},{"text":"web","start":1688500,"end":1688740,"confidence":0.9992676,"speaker":"A"},{"text":"authentication","start":1688740,"end":1689340,"confidence":0.99938965,"speaker":"A"},{"text":"token","start":1689340,"end":1689740,"confidence":0.99902344,"speaker":"A"},{"text":"there.","start":1689740,"end":1690060,"confidence":0.99560547,"speaker":"A"},{"text":"So","start":1691500,"end":1691780,"confidence":0.9921875,"speaker":"A"},{"text":"that's","start":1691780,"end":1692100,"confidence":0.9995117,"speaker":"A"},{"text":"actually","start":1692100,"end":1692300,"confidence":0.9995117,"speaker":"A"},{"text":"the","start":1692300,"end":1692540,"confidence":0.99609375,"speaker":"A"},{"text":"easier","start":1692540,"end":1692900,"confidence":0.99975586,"speaker":"A"},{"text":"of","start":1692900,"end":1693020,"confidence":0.9995117,"speaker":"A"},{"text":"the","start":1693020,"end":1693180,"confidence":0.99902344,"speaker":"A"},{"text":"two.","start":1693180,"end":1693500,"confidence":0.9926758,"speaker":"A"},{"text":"So","start":1694380,"end":1694660,"confidence":0.99902344,"speaker":"A"},{"text":"that","start":1694660,"end":1694820,"confidence":1,"speaker":"A"},{"text":"gives","start":1694820,"end":1695020,"confidence":1,"speaker":"A"},{"text":"you","start":1695020,"end":1695100,"confidence":1,"speaker":"A"},{"text":"the","start":1695100,"end":1695220,"confidence":0.9995117,"speaker":"A"},{"text":"private","start":1695220,"end":1695420,"confidence":1,"speaker":"A"},{"text":"database","start":1695420,"end":1695940,"confidence":0.9998372,"speaker":"A"},{"text":"for","start":1695940,"end":1696100,"confidence":0.9975586,"speaker":"A"},{"text":"the","start":1696100,"end":1696220,"confidence":0.9995117,"speaker":"A"},{"text":"public","start":1696220,"end":1696380,"confidence":1,"speaker":"A"},{"text":"database","start":1696380,"end":1696940,"confidence":0.99886066,"speaker":"A"},{"text":"is","start":1696940,"end":1697140,"confidence":0.98876953,"speaker":"A"},{"text":"where","start":1697140,"end":1697300,"confidence":0.99902344,"speaker":"A"},{"text":"you're","start":1697300,"end":1697500,"confidence":0.9975586,"speaker":"A"},{"text":"going","start":1697500,"end":1697580,"confidence":0.9355469,"speaker":"A"},{"text":"to","start":1697580,"end":1697660,"confidence":0.9980469,"speaker":"A"},{"text":"need","start":1697660,"end":1697820,"confidence":0.99853516,"speaker":"A"},{"text":"a","start":1697820,"end":1697990,"confidence":0.55908203,"speaker":"A"},{"text":"server","start":1698220,"end":1698460,"confidence":0.9975586,"speaker":"A"},{"text":"to","start":1698460,"end":1698620,"confidence":0.9536133,"speaker":"A"},{"text":"server","start":1698620,"end":1699020,"confidence":0.99902344,"speaker":"A"},{"text":"authentication.","start":1699020,"end":1699820,"confidence":0.99938965,"speaker":"A"},{"text":"And","start":1701340,"end":1701700,"confidence":0.98876953,"speaker":"A"},{"text":"so","start":1701700,"end":1701940,"confidence":0.9995117,"speaker":"A"},{"text":"to","start":1701940,"end":1702100,"confidence":0.9995117,"speaker":"A"},{"text":"do","start":1702100,"end":1702300,"confidence":0.9995117,"speaker":"A"},{"text":"that","start":1702300,"end":1702620,"confidence":0.9970703,"speaker":"A"},{"text":"it's","start":1703180,"end":1703540,"confidence":0.9996745,"speaker":"A"},{"text":"really","start":1703540,"end":1703820,"confidence":0.99853516,"speaker":"A"},{"text":"actually","start":1703820,"end":1704180,"confidence":0.99853516,"speaker":"A"},{"text":"not","start":1704180,"end":1704420,"confidence":1,"speaker":"A"},{"text":"as","start":1704420,"end":1704620,"confidence":0.99902344,"speaker":"A"},{"text":"bad","start":1704620,"end":1704820,"confidence":1,"speaker":"A"},{"text":"as","start":1704820,"end":1704980,"confidence":0.9995117,"speaker":"A"},{"text":"I","start":1704980,"end":1705140,"confidence":1,"speaker":"A"},{"text":"thought","start":1705140,"end":1705260,"confidence":1,"speaker":"A"},{"text":"it","start":1705260,"end":1705340,"confidence":0.9975586,"speaker":"A"},{"text":"was","start":1705340,"end":1705460,"confidence":0.9995117,"speaker":"A"},{"text":"going","start":1705460,"end":1705580,"confidence":0.8984375,"speaker":"A"},{"text":"to","start":1705580,"end":1705660,"confidence":1,"speaker":"A"},{"text":"be.","start":1705660,"end":1705900,"confidence":1,"speaker":"A"},{"text":"But","start":1705900,"end":1706300,"confidence":0.9975586,"speaker":"A"},{"text":"you","start":1706620,"end":1706940,"confidence":0.9995117,"speaker":"A"},{"text":"go","start":1706940,"end":1707220,"confidence":0.99902344,"speaker":"A"},{"text":"to","start":1707220,"end":1707500,"confidence":1,"speaker":"A"},{"text":"the","start":1707500,"end":1707700,"confidence":0.9995117,"speaker":"A"},{"text":"new","start":1707700,"end":1707980,"confidence":0.9970703,"speaker":"A"},{"text":"server","start":1708220,"end":1708620,"confidence":0.99731445,"speaker":"A"},{"text":"to","start":1708620,"end":1708740,"confidence":0.8359375,"speaker":"A"},{"text":"server","start":1708740,"end":1709140,"confidence":0.99731445,"speaker":"A"},{"text":"key,","start":1709140,"end":1709420,"confidence":0.99121094,"speaker":"A"},{"text":"put","start":1709420,"end":1709700,"confidence":0.9951172,"speaker":"A"},{"text":"in","start":1709700,"end":1709900,"confidence":0.9526367,"speaker":"A"},{"text":"a","start":1709900,"end":1710100,"confidence":0.9555664,"speaker":"A"},{"text":"name","start":1710100,"end":1710300,"confidence":0.9941406,"speaker":"A"},{"text":"you","start":1710300,"end":1710500,"confidence":0.99072266,"speaker":"A"},{"text":"want,","start":1710500,"end":1710780,"confidence":0.70458984,"speaker":"A"},{"text":"it'll","start":1711020,"end":1711460,"confidence":0.9889323,"speaker":"A"},{"text":"actually","start":1711460,"end":1711660,"confidence":0.99902344,"speaker":"A"},{"text":"give","start":1711660,"end":1711860,"confidence":1,"speaker":"A"},{"text":"you","start":1711860,"end":1712020,"confidence":0.9995117,"speaker":"A"},{"text":"the","start":1712020,"end":1712180,"confidence":0.9995117,"speaker":"A"},{"text":"command","start":1712180,"end":1712500,"confidence":0.9995117,"speaker":"A"},{"text":"you","start":1712500,"end":1712660,"confidence":0.9970703,"speaker":"A"},{"text":"need","start":1712660,"end":1712820,"confidence":0.99902344,"speaker":"A"},{"text":"to","start":1712820,"end":1712980,"confidence":1,"speaker":"A"},{"text":"run","start":1712980,"end":1713260,"confidence":0.9995117,"speaker":"A"},{"text":"and","start":1713340,"end":1713620,"confidence":0.99853516,"speaker":"A"},{"text":"then","start":1713620,"end":1713780,"confidence":0.9946289,"speaker":"A"},{"text":"you","start":1713780,"end":1713940,"confidence":0.99853516,"speaker":"A"},{"text":"just","start":1713940,"end":1714099,"confidence":0.9995117,"speaker":"A"},{"text":"paste","start":1714099,"end":1714420,"confidence":0.98950195,"speaker":"A"},{"text":"in","start":1714420,"end":1714580,"confidence":0.9951172,"speaker":"A"},{"text":"the","start":1714580,"end":1714700,"confidence":0.9995117,"speaker":"A"},{"text":"public","start":1714700,"end":1714900,"confidence":0.9995117,"speaker":"A"},{"text":"key","start":1714900,"end":1715180,"confidence":0.9995117,"speaker":"A"},{"text":"in","start":1715180,"end":1715380,"confidence":0.9169922,"speaker":"A"},{"text":"here.","start":1715380,"end":1715660,"confidence":0.9995117,"speaker":"A"},{"text":"That","start":1716380,"end":1716700,"confidence":0.9980469,"speaker":"A"},{"text":"gives","start":1716700,"end":1717060,"confidence":0.9995117,"speaker":"A"},{"text":"you.","start":1717060,"end":1717340,"confidence":0.9995117,"speaker":"A"},{"text":"That","start":1718780,"end":1719060,"confidence":0.8378906,"speaker":"A"},{"text":"will","start":1719060,"end":1719220,"confidence":0.9951172,"speaker":"A"},{"text":"give","start":1719220,"end":1719380,"confidence":1,"speaker":"A"},{"text":"you","start":1719380,"end":1719540,"confidence":1,"speaker":"A"},{"text":"everything","start":1719540,"end":1719780,"confidence":0.9995117,"speaker":"A"},{"text":"you","start":1719780,"end":1720020,"confidence":0.99902344,"speaker":"A"},{"text":"need.","start":1720020,"end":1720300,"confidence":0.9995117,"speaker":"A"},{"text":"So","start":1720860,"end":1721140,"confidence":0.9995117,"speaker":"A"},{"text":"here's","start":1721140,"end":1721540,"confidence":0.9949544,"speaker":"A"},{"text":"how","start":1721540,"end":1721780,"confidence":1,"speaker":"A"},{"text":"to","start":1721780,"end":1721940,"confidence":0.9995117,"speaker":"A"},{"text":"run","start":1721940,"end":1722100,"confidence":1,"speaker":"A"},{"text":"it.","start":1722100,"end":1722300,"confidence":0.99902344,"speaker":"A"},{"text":"Basically,","start":1722300,"end":1722780,"confidence":0.998291,"speaker":"A"},{"text":"sorry","start":1723990,"end":1724190,"confidence":0.9773763,"speaker":"A"},{"text":"about","start":1724190,"end":1724350,"confidence":0.9819336,"speaker":"A"},{"text":"that.","start":1724350,"end":1724630,"confidence":0.9941406,"speaker":"A"},{"text":"We","start":1737190,"end":1737470,"confidence":0.7998047,"speaker":"A"},{"text":"just","start":1737470,"end":1737670,"confidence":0.99853516,"speaker":"A"},{"text":"run","start":1737670,"end":1737870,"confidence":0.9975586,"speaker":"A"},{"text":"that.","start":1737870,"end":1738150,"confidence":0.9970703,"speaker":"A"},{"text":"That","start":1738470,"end":1738750,"confidence":0.9995117,"speaker":"A"},{"text":"gives","start":1738750,"end":1738950,"confidence":0.99975586,"speaker":"A"},{"text":"us","start":1738950,"end":1739070,"confidence":1,"speaker":"A"},{"text":"the","start":1739070,"end":1739230,"confidence":0.9995117,"speaker":"A"},{"text":"key.","start":1739230,"end":1739510,"confidence":0.9995117,"speaker":"A"},{"text":"We","start":1740710,"end":1740990,"confidence":0.99902344,"speaker":"A"},{"text":"can","start":1740990,"end":1741150,"confidence":0.9995117,"speaker":"A"},{"text":"go","start":1741150,"end":1741310,"confidence":0.99902344,"speaker":"A"},{"text":"ahead","start":1741310,"end":1741550,"confidence":0.9995117,"speaker":"A"},{"text":"and","start":1741550,"end":1741910,"confidence":0.9970703,"speaker":"A"},{"text":"get","start":1742070,"end":1742350,"confidence":1,"speaker":"A"},{"text":"the","start":1742350,"end":1742510,"confidence":1,"speaker":"A"},{"text":"public","start":1742510,"end":1742750,"confidence":1,"speaker":"A"},{"text":"key.","start":1742750,"end":1743110,"confidence":0.9995117,"speaker":"A"},{"text":"We","start":1743190,"end":1743470,"confidence":0.9980469,"speaker":"A"},{"text":"can","start":1743470,"end":1743750,"confidence":0.9995117,"speaker":"A"},{"text":"also","start":1743910,"end":1744270,"confidence":0.99902344,"speaker":"A"},{"text":"pipe","start":1744270,"end":1744670,"confidence":0.9607747,"speaker":"A"},{"text":"it","start":1744670,"end":1744870,"confidence":0.9975586,"speaker":"A"},{"text":"to","start":1744870,"end":1745070,"confidence":0.9975586,"speaker":"A"},{"text":"PB","start":1745070,"end":1745390,"confidence":0.79541016,"speaker":"A"},{"text":"Copy","start":1745390,"end":1745990,"confidence":0.9637044,"speaker":"A"},{"text":"and","start":1746470,"end":1746750,"confidence":0.9321289,"speaker":"A"},{"text":"then","start":1746750,"end":1746910,"confidence":0.98779297,"speaker":"A"},{"text":"all","start":1746910,"end":1747070,"confidence":0.9995117,"speaker":"A"},{"text":"we","start":1747070,"end":1747190,"confidence":0.9995117,"speaker":"A"},{"text":"have","start":1747190,"end":1747310,"confidence":0.95947266,"speaker":"A"},{"text":"to","start":1747310,"end":1747430,"confidence":0.99609375,"speaker":"A"},{"text":"do","start":1747430,"end":1747590,"confidence":0.99609375,"speaker":"A"},{"text":"is","start":1747590,"end":1747830,"confidence":0.99902344,"speaker":"A"},{"text":"paste","start":1747830,"end":1748110,"confidence":0.9172363,"speaker":"A"},{"text":"that","start":1748110,"end":1748310,"confidence":0.99560547,"speaker":"A"},{"text":"in","start":1748310,"end":1748510,"confidence":0.9970703,"speaker":"A"},{"text":"the","start":1748510,"end":1748670,"confidence":0.99853516,"speaker":"A"},{"text":"box","start":1748670,"end":1749030,"confidence":0.99780273,"speaker":"A"},{"text":"over","start":1750370,"end":1750570,"confidence":0.9951172,"speaker":"A"},{"text":"here.","start":1750570,"end":1750930,"confidence":0.9995117,"speaker":"A"},{"text":"There","start":1757970,"end":1758250,"confidence":0.98046875,"speaker":"A"},{"text":"we","start":1758250,"end":1758410,"confidence":0.5283203,"speaker":"A"},{"text":"go.","start":1758410,"end":1758690,"confidence":1,"speaker":"A"},{"text":"It's","start":1765890,"end":1766250,"confidence":0.9930013,"speaker":"A"},{"text":"pretty","start":1766250,"end":1766570,"confidence":0.9998372,"speaker":"A"},{"text":"complicated","start":1766570,"end":1767250,"confidence":1,"speaker":"A"},{"text":"to","start":1767250,"end":1767490,"confidence":0.9995117,"speaker":"A"},{"text":"use","start":1767490,"end":1767770,"confidence":1,"speaker":"A"},{"text":"the","start":1767770,"end":1768010,"confidence":0.9995117,"speaker":"A"},{"text":"server","start":1768010,"end":1768450,"confidence":0.99975586,"speaker":"A"},{"text":"key.","start":1768450,"end":1768770,"confidence":0.99560547,"speaker":"A"},{"text":"We","start":1770050,"end":1770330,"confidence":0.9951172,"speaker":"A"},{"text":"can","start":1770330,"end":1770490,"confidence":0.99902344,"speaker":"A"},{"text":"spell","start":1770490,"end":1770770,"confidence":0.9838867,"speaker":"A"},{"text":"on","start":1770770,"end":1771050,"confidence":0.8208008,"speaker":"A"},{"text":"the","start":1771050,"end":1771250,"confidence":0.99658203,"speaker":"A"},{"text":"miskit","start":1771250,"end":1771690,"confidence":0.9238281,"speaker":"A"},{"text":"code","start":1771690,"end":1771970,"confidence":0.99348956,"speaker":"A"},{"text":"on","start":1771970,"end":1772090,"confidence":0.9975586,"speaker":"A"},{"text":"how","start":1772090,"end":1772250,"confidence":0.9995117,"speaker":"A"},{"text":"to","start":1772250,"end":1772410,"confidence":0.99902344,"speaker":"A"},{"text":"do","start":1772410,"end":1772570,"confidence":0.9995117,"speaker":"A"},{"text":"it","start":1772570,"end":1772850,"confidence":0.9995117,"speaker":"A"},{"text":"because","start":1773170,"end":1773450,"confidence":0.9663086,"speaker":"A"},{"text":"it","start":1773450,"end":1773610,"confidence":0.9995117,"speaker":"A"},{"text":"does","start":1773610,"end":1773810,"confidence":0.99902344,"speaker":"A"},{"text":"a","start":1773810,"end":1773970,"confidence":0.9995117,"speaker":"A"},{"text":"lot","start":1773970,"end":1774050,"confidence":1,"speaker":"A"},{"text":"of","start":1774050,"end":1774130,"confidence":0.9980469,"speaker":"A"},{"text":"that","start":1774130,"end":1774290,"confidence":0.99560547,"speaker":"A"},{"text":"work","start":1774290,"end":1774530,"confidence":1,"speaker":"A"},{"text":"for","start":1774530,"end":1774730,"confidence":0.99902344,"speaker":"A"},{"text":"you","start":1774730,"end":1774930,"confidence":0.9995117,"speaker":"A"},{"text":"if","start":1774930,"end":1775170,"confidence":0.59228516,"speaker":"A"},{"text":"you","start":1775170,"end":1775330,"confidence":0.9995117,"speaker":"A"},{"text":"have","start":1775330,"end":1775450,"confidence":0.9995117,"speaker":"A"},{"text":"it.","start":1775450,"end":1775730,"confidence":0.9916992,"speaker":"A"},{"text":"But","start":1776610,"end":1776730,"confidence":0.99121094,"speaker":"A"},{"text":"you","start":1776730,"end":1776890,"confidence":0.9995117,"speaker":"A"},{"text":"will","start":1776890,"end":1777090,"confidence":0.9995117,"speaker":"A"},{"text":"need","start":1777090,"end":1777410,"confidence":0.9995117,"speaker":"A"},{"text":"the,","start":1777650,"end":1778050,"confidence":0.8984375,"speaker":"A"},{"text":"the","start":1779170,"end":1779490,"confidence":0.98876953,"speaker":"A"},{"text":"private","start":1779490,"end":1779810,"confidence":0.9995117,"speaker":"A"},{"text":"key,","start":1779890,"end":1780290,"confidence":0.9975586,"speaker":"A"},{"text":"the","start":1780290,"end":1780570,"confidence":0.99121094,"speaker":"A"},{"text":"key","start":1780570,"end":1780810,"confidence":0.9946289,"speaker":"A"},{"text":"id,","start":1780810,"end":1781170,"confidence":0.98583984,"speaker":"A"},{"text":"I","start":1782290,"end":1782570,"confidence":0.90771484,"speaker":"A"},{"text":"think,","start":1782570,"end":1782850,"confidence":0.9980469,"speaker":"A"},{"text":"I","start":1783170,"end":1783450,"confidence":0.8652344,"speaker":"A"},{"text":"think","start":1783450,"end":1783610,"confidence":0.9868164,"speaker":"A"},{"text":"that's","start":1783610,"end":1783810,"confidence":0.9995117,"speaker":"A"},{"text":"it.","start":1783810,"end":1784050,"confidence":0.9941406,"speaker":"A"},{"text":"And","start":1784370,"end":1784650,"confidence":0.9921875,"speaker":"A"},{"text":"then","start":1784650,"end":1784890,"confidence":0.94677734,"speaker":"A"},{"text":"you","start":1784890,"end":1785130,"confidence":0.99658203,"speaker":"A"},{"text":"should","start":1785130,"end":1785290,"confidence":1,"speaker":"A"},{"text":"be","start":1785290,"end":1785490,"confidence":1,"speaker":"A"},{"text":"good","start":1785490,"end":1785810,"confidence":0.9995117,"speaker":"A"},{"text":"with","start":1786130,"end":1786490,"confidence":0.9975586,"speaker":"A"},{"text":"having","start":1786490,"end":1786810,"confidence":0.9555664,"speaker":"A"},{"text":"access","start":1786810,"end":1787170,"confidence":1,"speaker":"A"},{"text":"now","start":1787170,"end":1787490,"confidence":0.9975586,"speaker":"A"},{"text":"to","start":1787490,"end":1787770,"confidence":0.9995117,"speaker":"A"},{"text":"the","start":1787770,"end":1788010,"confidence":0.9995117,"speaker":"A"},{"text":"public","start":1788010,"end":1788290,"confidence":0.9995117,"speaker":"A"},{"text":"database.","start":1789330,"end":1790130,"confidence":0.99902344,"speaker":"A"},{"text":"So","start":1790850,"end":1791250,"confidence":0.98876953,"speaker":"A"},{"text":"just","start":1791570,"end":1791889,"confidence":0.9995117,"speaker":"A"},{"text":"to","start":1791889,"end":1792050,"confidence":0.99853516,"speaker":"A"},{"text":"go","start":1792050,"end":1792209,"confidence":0.99902344,"speaker":"A"},{"text":"over,","start":1792209,"end":1792530,"confidence":1,"speaker":"A"},{"text":"there's","start":1792610,"end":1793050,"confidence":0.9892578,"speaker":"A"},{"text":"differences","start":1793050,"end":1793450,"confidence":0.9995117,"speaker":"A"},{"text":"between","start":1793450,"end":1793770,"confidence":1,"speaker":"A"},{"text":"the","start":1793770,"end":1793970,"confidence":0.9995117,"speaker":"A"},{"text":"public","start":1793970,"end":1794210,"confidence":1,"speaker":"A"},{"text":"and","start":1794210,"end":1794490,"confidence":0.99902344,"speaker":"A"},{"text":"private","start":1794490,"end":1794730,"confidence":1,"speaker":"A"},{"text":"database.","start":1794730,"end":1795490,"confidence":0.99820966,"speaker":"A"},{"text":"So","start":1797170,"end":1797570,"confidence":0.99609375,"speaker":"A"},{"text":"this","start":1797730,"end":1798050,"confidence":0.9995117,"speaker":"A"},{"text":"is","start":1798050,"end":1798370,"confidence":0.9995117,"speaker":"A"},{"text":"query.","start":1798530,"end":1799090,"confidence":0.9975586,"speaker":"A"},{"text":"You","start":1799570,"end":1799810,"confidence":0.9995117,"speaker":"A"},{"text":"can","start":1799810,"end":1799930,"confidence":0.5439453,"speaker":"A"},{"text":"see","start":1799930,"end":1800090,"confidence":0.99609375,"speaker":"A"},{"text":"my","start":1800090,"end":1800250,"confidence":0.8847656,"speaker":"A"},{"text":"cursor,","start":1800250,"end":1800650,"confidence":0.9938151,"speaker":"A"},{"text":"right?","start":1800650,"end":1800930,"confidence":0.97265625,"speaker":"A"},{"text":"Query","start":1800930,"end":1801330,"confidence":0.9904785,"speaker":"A"},{"text":"and","start":1801330,"end":1801530,"confidence":0.53759766,"speaker":"A"},{"text":"lookup","start":1801530,"end":1802010,"confidence":0.94018555,"speaker":"A"},{"text":"of","start":1802010,"end":1802330,"confidence":0.9916992,"speaker":"A"},{"text":"records","start":1802330,"end":1803010,"confidence":0.99975586,"speaker":"A"},{"text":"is","start":1803010,"end":1803290,"confidence":0.9995117,"speaker":"A"},{"text":"available","start":1803290,"end":1803570,"confidence":0.9995117,"speaker":"A"},{"text":"on","start":1803650,"end":1803970,"confidence":0.99853516,"speaker":"A"},{"text":"all","start":1803970,"end":1804290,"confidence":0.99658203,"speaker":"A"},{"text":"but","start":1805270,"end":1805510,"confidence":0.9897461,"speaker":"A"},{"text":"file","start":1805590,"end":1806030,"confidence":0.9970703,"speaker":"A"},{"text":"changes","start":1806030,"end":1806630,"confidence":0.9992676,"speaker":"A"},{"text":"or,","start":1806790,"end":1807110,"confidence":0.97314453,"speaker":"A"},{"text":"excuse","start":1807110,"end":1807430,"confidence":0.99820966,"speaker":"A"},{"text":"me,","start":1807430,"end":1807670,"confidence":0.9995117,"speaker":"A"},{"text":"record","start":1807990,"end":1808350,"confidence":0.99609375,"speaker":"A"},{"text":"changes.","start":1808350,"end":1808830,"confidence":0.99975586,"speaker":"A"},{"text":"It's","start":1808830,"end":1809070,"confidence":0.8819987,"speaker":"A"},{"text":"not","start":1809070,"end":1809230,"confidence":1,"speaker":"A"},{"text":"available","start":1809230,"end":1809510,"confidence":0.99853516,"speaker":"A"},{"text":"on","start":1809830,"end":1810150,"confidence":0.9160156,"speaker":"A"},{"text":"public","start":1810150,"end":1810470,"confidence":0.9995117,"speaker":"A"},{"text":"zones,","start":1810950,"end":1811390,"confidence":0.9909668,"speaker":"A"},{"text":"aren't","start":1811390,"end":1811670,"confidence":0.9958496,"speaker":"A"},{"text":"really","start":1811670,"end":1811830,"confidence":1,"speaker":"A"},{"text":"available","start":1811830,"end":1812150,"confidence":0.99853516,"speaker":"A"},{"text":"in","start":1812150,"end":1812430,"confidence":0.9394531,"speaker":"A"},{"text":"public","start":1812430,"end":1812710,"confidence":1,"speaker":"A"},{"text":"zone","start":1812790,"end":1813190,"confidence":0.96240234,"speaker":"A"},{"text":"changes","start":1813190,"end":1813550,"confidence":0.8989258,"speaker":"A"},{"text":"aren't","start":1813550,"end":1813870,"confidence":0.9959717,"speaker":"A"},{"text":"available","start":1813870,"end":1814150,"confidence":1,"speaker":"A"},{"text":"in","start":1814470,"end":1814750,"confidence":0.9667969,"speaker":"A"},{"text":"public","start":1814750,"end":1815030,"confidence":1,"speaker":"A"},{"text":"notifications.","start":1815670,"end":1816470,"confidence":0.9949544,"speaker":"A"},{"text":"Zone","start":1816550,"end":1816950,"confidence":0.94677734,"speaker":"A"},{"text":"notifications","start":1816950,"end":1817630,"confidence":0.9996745,"speaker":"A"},{"text":"aren't","start":1817630,"end":1817950,"confidence":0.9765625,"speaker":"A"},{"text":"available","start":1817950,"end":1818230,"confidence":1,"speaker":"A"},{"text":"in","start":1818310,"end":1818590,"confidence":0.9941406,"speaker":"A"},{"text":"public,","start":1818590,"end":1818870,"confidence":1,"speaker":"A"},{"text":"but","start":1819670,"end":1820070,"confidence":0.9921875,"speaker":"A"},{"text":"query","start":1820070,"end":1820550,"confidence":0.82421875,"speaker":"A"},{"text":"notifications","start":1820709,"end":1821510,"confidence":0.9996745,"speaker":"A"},{"text":"are.","start":1821590,"end":1821990,"confidence":0.9902344,"speaker":"A"},{"text":"And","start":1821990,"end":1822390,"confidence":0.9921875,"speaker":"A"},{"text":"you","start":1822390,"end":1822630,"confidence":0.9995117,"speaker":"A"},{"text":"can","start":1822630,"end":1822750,"confidence":0.9995117,"speaker":"A"},{"text":"also","start":1822750,"end":1822990,"confidence":0.9995117,"speaker":"A"},{"text":"do","start":1822990,"end":1823350,"confidence":1,"speaker":"A"},{"text":"any","start":1823350,"end":1823750,"confidence":0.99853516,"speaker":"A"},{"text":"stuff","start":1823750,"end":1824150,"confidence":0.9996745,"speaker":"A"},{"text":"with","start":1824150,"end":1824470,"confidence":0.98876953,"speaker":"A"},{"text":"assets","start":1824710,"end":1825270,"confidence":0.7792969,"speaker":"A"},{"text":"which","start":1825350,"end":1825630,"confidence":0.99853516,"speaker":"A"},{"text":"are","start":1825630,"end":1825790,"confidence":1,"speaker":"A"},{"text":"basically","start":1825790,"end":1826190,"confidence":0.99975586,"speaker":"A"},{"text":"binary","start":1826190,"end":1826710,"confidence":0.9995117,"speaker":"A"},{"text":"files.","start":1826710,"end":1827030,"confidence":0.99194336,"speaker":"A"},{"text":"You","start":1827030,"end":1827190,"confidence":0.99853516,"speaker":"A"},{"text":"can","start":1827190,"end":1827310,"confidence":0.99853516,"speaker":"A"},{"text":"also","start":1827310,"end":1827470,"confidence":0.9995117,"speaker":"A"},{"text":"do","start":1827470,"end":1827630,"confidence":0.9995117,"speaker":"A"},{"text":"that","start":1827630,"end":1827910,"confidence":0.99902344,"speaker":"A"},{"text":"in","start":1828310,"end":1828670,"confidence":0.5600586,"speaker":"A"},{"text":"all","start":1828670,"end":1828910,"confidence":0.9995117,"speaker":"A"},{"text":"of","start":1828910,"end":1829070,"confidence":0.99902344,"speaker":"A"},{"text":"them.","start":1829070,"end":1829350,"confidence":0.9145508,"speaker":"A"},{"text":"You","start":1830630,"end":1830910,"confidence":0.99658203,"speaker":"A"},{"text":"can't","start":1830910,"end":1831230,"confidence":0.9586589,"speaker":"A"},{"text":"do","start":1831230,"end":1831590,"confidence":1,"speaker":"A"},{"text":"query","start":1831750,"end":1832190,"confidence":0.970459,"speaker":"A"},{"text":"notifications","start":1832190,"end":1832990,"confidence":0.99934894,"speaker":"A"},{"text":"on","start":1832990,"end":1833270,"confidence":0.98046875,"speaker":"A"},{"text":"shared.","start":1833270,"end":1833830,"confidence":0.99780273,"speaker":"A"},{"text":"Shared","start":1834470,"end":1834910,"confidence":0.9873047,"speaker":"A"},{"text":"would","start":1834910,"end":1835110,"confidence":0.5698242,"speaker":"A"},{"text":"essentially","start":1835110,"end":1835590,"confidence":0.99902344,"speaker":"A"},{"text":"work","start":1835590,"end":1835870,"confidence":1,"speaker":"A"},{"text":"like","start":1835870,"end":1836110,"confidence":0.9980469,"speaker":"A"},{"text":"private","start":1836110,"end":1836390,"confidence":0.99902344,"speaker":"A"},{"text":"essentially.","start":1836850,"end":1837410,"confidence":0.9968262,"speaker":"A"},{"text":"So","start":1837490,"end":1837890,"confidence":0.9946289,"speaker":"A"},{"text":"it's","start":1839090,"end":1839410,"confidence":0.9995117,"speaker":"A"},{"text":"just","start":1839410,"end":1839530,"confidence":0.9995117,"speaker":"A"},{"text":"a","start":1839530,"end":1839650,"confidence":0.9995117,"speaker":"A"},{"text":"matter","start":1839650,"end":1839810,"confidence":1,"speaker":"A"},{"text":"of","start":1839810,"end":1840130,"confidence":0.99902344,"speaker":"A"},{"text":"who.","start":1840130,"end":1840530,"confidence":0.77685547,"speaker":"A"},{"text":"Who's","start":1840530,"end":1840930,"confidence":0.9977214,"speaker":"A"},{"text":"the","start":1840930,"end":1841050,"confidence":0.99853516,"speaker":"A"},{"text":"owner","start":1841050,"end":1841370,"confidence":1,"speaker":"A"},{"text":"and","start":1841370,"end":1841570,"confidence":0.99609375,"speaker":"A"},{"text":"how","start":1841570,"end":1841810,"confidence":0.9995117,"speaker":"A"},{"text":"is","start":1841810,"end":1841970,"confidence":0.94970703,"speaker":"A"},{"text":"it","start":1841970,"end":1842090,"confidence":0.99902344,"speaker":"A"},{"text":"shared.","start":1842090,"end":1842610,"confidence":0.9968262,"speaker":"A"},{"text":"So","start":1844690,"end":1844930,"confidence":0.99658203,"speaker":"A"},{"text":"one","start":1844930,"end":1845050,"confidence":0.9794922,"speaker":"A"},{"text":"of","start":1845050,"end":1845210,"confidence":1,"speaker":"A"},{"text":"the","start":1845210,"end":1845450,"confidence":0.9995117,"speaker":"A"},{"text":"big","start":1845450,"end":1845730,"confidence":1,"speaker":"A"},{"text":"challenges","start":1845730,"end":1846370,"confidence":0.96468097,"speaker":"A"},{"text":"I","start":1846450,"end":1846730,"confidence":0.99853516,"speaker":"A"},{"text":"think","start":1846730,"end":1846890,"confidence":1,"speaker":"A"},{"text":"we've","start":1846890,"end":1847170,"confidence":0.9977214,"speaker":"A"},{"text":"all","start":1847170,"end":1847330,"confidence":0.9995117,"speaker":"A"},{"text":"faced","start":1847330,"end":1847650,"confidence":0.95825195,"speaker":"A"},{"text":"this","start":1847650,"end":1847810,"confidence":0.99072266,"speaker":"A"},{"text":"when","start":1847810,"end":1848010,"confidence":0.99609375,"speaker":"A"},{"text":"we've","start":1848010,"end":1848370,"confidence":0.98095703,"speaker":"A"},{"text":"dealt","start":1848370,"end":1848650,"confidence":0.9992676,"speaker":"A"},{"text":"with","start":1848650,"end":1848810,"confidence":1,"speaker":"A"},{"text":"certain","start":1848810,"end":1849010,"confidence":0.9995117,"speaker":"A"},{"text":"web","start":1849010,"end":1849290,"confidence":0.99902344,"speaker":"A"},{"text":"services","start":1849290,"end":1849570,"confidence":0.9995117,"speaker":"A"},{"text":"is","start":1850530,"end":1850930,"confidence":0.98876953,"speaker":"A"},{"text":"field","start":1851410,"end":1851810,"confidence":0.9897461,"speaker":"A"},{"text":"type","start":1851970,"end":1852449,"confidence":0.810791,"speaker":"A"},{"text":"polymorphism.","start":1852449,"end":1853370,"confidence":0.9991862,"speaker":"A"},{"text":"If","start":1853370,"end":1853570,"confidence":1,"speaker":"A"},{"text":"you've","start":1853570,"end":1853730,"confidence":0.9998372,"speaker":"A"},{"text":"done","start":1853730,"end":1853890,"confidence":0.9975586,"speaker":"A"},{"text":"JSON","start":1853890,"end":1854370,"confidence":0.7998047,"speaker":"A"},{"text":"where","start":1854370,"end":1854650,"confidence":0.87939453,"speaker":"A"},{"text":"you","start":1854650,"end":1854850,"confidence":1,"speaker":"A"},{"text":"don't","start":1854850,"end":1855090,"confidence":0.9996745,"speaker":"A"},{"text":"know","start":1855090,"end":1855210,"confidence":0.99902344,"speaker":"A"},{"text":"what","start":1855210,"end":1855370,"confidence":0.9995117,"speaker":"A"},{"text":"type","start":1855370,"end":1855730,"confidence":0.9946289,"speaker":"A"},{"text":"you're","start":1855730,"end":1855970,"confidence":1,"speaker":"A"},{"text":"getting","start":1855970,"end":1856130,"confidence":0.9995117,"speaker":"A"},{"text":"back","start":1856130,"end":1856370,"confidence":0.9980469,"speaker":"A"},{"text":"or","start":1856370,"end":1856570,"confidence":0.9980469,"speaker":"A"},{"text":"what","start":1856570,"end":1856730,"confidence":0.98876953,"speaker":"A"},{"text":"data","start":1856730,"end":1856930,"confidence":0.9980469,"speaker":"A"},{"text":"you're","start":1856930,"end":1857170,"confidence":0.9995117,"speaker":"A"},{"text":"getting","start":1857170,"end":1857370,"confidence":0.9916992,"speaker":"A"},{"text":"back,","start":1857370,"end":1857730,"confidence":0.9526367,"speaker":"A"},{"text":"this","start":1858050,"end":1858330,"confidence":0.9995117,"speaker":"A"},{"text":"can","start":1858330,"end":1858490,"confidence":0.99902344,"speaker":"A"},{"text":"Be","start":1858490,"end":1858610,"confidence":1,"speaker":"A"},{"text":"a","start":1858610,"end":1858690,"confidence":0.9995117,"speaker":"A"},{"text":"bit","start":1858690,"end":1858850,"confidence":0.99902344,"speaker":"A"},{"text":"challenging.","start":1858850,"end":1859410,"confidence":0.9601237,"speaker":"A"},{"text":"So","start":1860530,"end":1860930,"confidence":0.9951172,"speaker":"A"},{"text":"if","start":1861730,"end":1862050,"confidence":0.6791992,"speaker":"A"},{"text":"you","start":1862050,"end":1862250,"confidence":1,"speaker":"A"},{"text":"look","start":1862250,"end":1862410,"confidence":1,"speaker":"A"},{"text":"at","start":1862410,"end":1862610,"confidence":0.9995117,"speaker":"A"},{"text":"the","start":1862610,"end":1862850,"confidence":0.9980469,"speaker":"A"},{"text":"documentation","start":1862850,"end":1863650,"confidence":0.99902344,"speaker":"A"},{"text":"in","start":1864290,"end":1864490,"confidence":0.78466797,"speaker":"A"},{"text":"Web","start":1864490,"end":1864810,"confidence":0.9890137,"speaker":"A"},{"text":"Services","start":1864810,"end":1865090,"confidence":0.99902344,"speaker":"A"},{"text":"Reference,","start":1865090,"end":1865810,"confidence":0.9918213,"speaker":"A"},{"text":"there","start":1866850,"end":1867210,"confidence":0.9921875,"speaker":"A"},{"text":"is","start":1867210,"end":1867570,"confidence":0.99902344,"speaker":"A"},{"text":"a,","start":1867890,"end":1868290,"confidence":0.99853516,"speaker":"A"},{"text":"there's","start":1869090,"end":1869610,"confidence":0.9824219,"speaker":"A"},{"text":"a","start":1869610,"end":1869890,"confidence":0.99902344,"speaker":"A"},{"text":"page","start":1869890,"end":1870290,"confidence":0.9951172,"speaker":"A"},{"text":"called","start":1870290,"end":1870530,"confidence":0.9995117,"speaker":"A"},{"text":"types","start":1870530,"end":1870810,"confidence":0.87719727,"speaker":"A"},{"text":"and","start":1870810,"end":1870970,"confidence":0.9536133,"speaker":"A"},{"text":"dictionaries","start":1870970,"end":1871650,"confidence":0.99609375,"speaker":"A"},{"text":"and","start":1871650,"end":1872010,"confidence":0.99902344,"speaker":"A"},{"text":"there","start":1872010,"end":1872290,"confidence":0.9980469,"speaker":"A"},{"text":"is","start":1872290,"end":1872610,"confidence":0.99609375,"speaker":"A"},{"text":"types.","start":1872610,"end":1873170,"confidence":0.9255371,"speaker":"A"},{"text":"There's","start":1874050,"end":1874410,"confidence":0.98860675,"speaker":"A"},{"text":"different","start":1874410,"end":1874610,"confidence":1,"speaker":"A"},{"text":"type","start":1874610,"end":1875010,"confidence":0.83618164,"speaker":"A"},{"text":"values","start":1875010,"end":1875530,"confidence":0.9992676,"speaker":"A"},{"text":"for","start":1875530,"end":1875690,"confidence":1,"speaker":"A"},{"text":"each","start":1875690,"end":1875930,"confidence":1,"speaker":"A"},{"text":"field.","start":1875930,"end":1876250,"confidence":1,"speaker":"A"},{"text":"If","start":1876250,"end":1876450,"confidence":1,"speaker":"A"},{"text":"you're","start":1876450,"end":1876610,"confidence":1,"speaker":"A"},{"text":"familiar","start":1876610,"end":1876890,"confidence":1,"speaker":"A"},{"text":"with","start":1876890,"end":1877050,"confidence":0.9995117,"speaker":"A"},{"text":"CloudKit,","start":1877050,"end":1877530,"confidence":0.953125,"speaker":"A"},{"text":"you've","start":1877530,"end":1877730,"confidence":0.99886066,"speaker":"A"},{"text":"seen","start":1877730,"end":1877890,"confidence":0.9995117,"speaker":"A"},{"text":"this,","start":1877890,"end":1878130,"confidence":0.9980469,"speaker":"A"},{"text":"right?","start":1878130,"end":1878450,"confidence":0.99853516,"speaker":"A"},{"text":"So","start":1879170,"end":1879570,"confidence":0.9995117,"speaker":"A"},{"text":"you","start":1879570,"end":1879850,"confidence":0.9995117,"speaker":"A"},{"text":"have","start":1879850,"end":1880089,"confidence":1,"speaker":"A"},{"text":"an","start":1880089,"end":1880329,"confidence":0.99853516,"speaker":"A"},{"text":"asset","start":1880329,"end":1880650,"confidence":0.9995117,"speaker":"A"},{"text":"which","start":1880650,"end":1880850,"confidence":1,"speaker":"A"},{"text":"is","start":1880850,"end":1881050,"confidence":0.9995117,"speaker":"A"},{"text":"basically","start":1881050,"end":1881490,"confidence":1,"speaker":"A"},{"text":"a,","start":1882210,"end":1882610,"confidence":0.9838867,"speaker":"A"},{"text":"a","start":1884290,"end":1884690,"confidence":0.9995117,"speaker":"A"},{"text":"binary","start":1884690,"end":1885330,"confidence":0.9998372,"speaker":"A"},{"text":"file.","start":1885330,"end":1885810,"confidence":0.69873047,"speaker":"A"},{"text":"You","start":1886850,"end":1887170,"confidence":1,"speaker":"A"},{"text":"have","start":1887170,"end":1887490,"confidence":1,"speaker":"A"},{"text":"bytes","start":1887490,"end":1888210,"confidence":0.8411458,"speaker":"A"},{"text":"which","start":1889090,"end":1889410,"confidence":1,"speaker":"A"},{"text":"is","start":1889410,"end":1889650,"confidence":0.9995117,"speaker":"A"},{"text":"essentially","start":1889650,"end":1890130,"confidence":0.99902344,"speaker":"A"},{"text":"a","start":1890130,"end":1890450,"confidence":0.95996094,"speaker":"A"},{"text":"60","start":1890530,"end":1890930,"confidence":0.9458,"speaker":"A"},{"text":"byte","start":1891170,"end":1891650,"confidence":0.9658203,"speaker":"A"},{"text":"base","start":1891860,"end":1892100,"confidence":0.8461914,"speaker":"A"},{"text":"64","start":1892100,"end":1892580,"confidence":0.99829,"speaker":"A"},{"text":"encoded","start":1892580,"end":1893140,"confidence":0.9967448,"speaker":"A"},{"text":"string,","start":1893140,"end":1893620,"confidence":0.9970703,"speaker":"A"},{"text":"date","start":1894740,"end":1895140,"confidence":0.98095703,"speaker":"A"},{"text":"type","start":1895140,"end":1895580,"confidence":0.9716797,"speaker":"A"},{"text":"which","start":1895580,"end":1895820,"confidence":0.9995117,"speaker":"A"},{"text":"is","start":1895820,"end":1896060,"confidence":0.99658203,"speaker":"A"},{"text":"returned","start":1896060,"end":1896580,"confidence":0.98876953,"speaker":"A"},{"text":"as","start":1896580,"end":1896700,"confidence":0.99902344,"speaker":"A"},{"text":"a","start":1896700,"end":1896860,"confidence":0.9995117,"speaker":"A"},{"text":"number.","start":1896860,"end":1897140,"confidence":0.99560547,"speaker":"A"},{"text":"Double","start":1897780,"end":1898220,"confidence":0.9511719,"speaker":"A"},{"text":"is","start":1898220,"end":1898460,"confidence":0.98779297,"speaker":"A"},{"text":"returned","start":1898460,"end":1898860,"confidence":0.954834,"speaker":"A"},{"text":"as","start":1898860,"end":1899020,"confidence":0.9951172,"speaker":"A"},{"text":"a","start":1899020,"end":1899140,"confidence":0.99853516,"speaker":"A"},{"text":"number","start":1899140,"end":1899380,"confidence":0.99658203,"speaker":"A"},{"text":"because","start":1899940,"end":1900220,"confidence":0.7080078,"speaker":"A"},{"text":"These","start":1900220,"end":1900380,"confidence":0.99658203,"speaker":"A"},{"text":"are","start":1900380,"end":1900500,"confidence":0.9995117,"speaker":"A"},{"text":"the","start":1900500,"end":1900620,"confidence":0.9995117,"speaker":"A"},{"text":"JavaScript","start":1900620,"end":1901220,"confidence":0.9517415,"speaker":"A"},{"text":"types.","start":1901220,"end":1901620,"confidence":0.76464844,"speaker":"A"},{"text":"Int","start":1902260,"end":1902660,"confidence":0.57714844,"speaker":"A"},{"text":"is","start":1902820,"end":1903220,"confidence":0.99609375,"speaker":"A"},{"text":"returned","start":1903540,"end":1904060,"confidence":0.9616699,"speaker":"A"},{"text":"as","start":1904060,"end":1904220,"confidence":0.99902344,"speaker":"A"},{"text":"a","start":1904220,"end":1904340,"confidence":0.99902344,"speaker":"A"},{"text":"number","start":1904340,"end":1904580,"confidence":0.99609375,"speaker":"A"},{"text":"and","start":1905700,"end":1905980,"confidence":0.9946289,"speaker":"A"},{"text":"then","start":1905980,"end":1906140,"confidence":0.99902344,"speaker":"A"},{"text":"there's","start":1906140,"end":1906420,"confidence":0.85302734,"speaker":"A"},{"text":"location","start":1906420,"end":1906980,"confidence":0.99902344,"speaker":"A"},{"text":"reference","start":1907540,"end":1908260,"confidence":0.8996582,"speaker":"A"},{"text":"and","start":1909300,"end":1909620,"confidence":0.9892578,"speaker":"A"},{"text":"then","start":1909620,"end":1909940,"confidence":0.9980469,"speaker":"A"},{"text":"string","start":1910020,"end":1910500,"confidence":0.9926758,"speaker":"A"},{"text":"and","start":1910500,"end":1910740,"confidence":0.98828125,"speaker":"A"},{"text":"list.","start":1910740,"end":1911060,"confidence":0.99658203,"speaker":"A"},{"text":"And","start":1911620,"end":1912020,"confidence":0.9951172,"speaker":"A"},{"text":"how","start":1912100,"end":1912420,"confidence":0.9980469,"speaker":"A"},{"text":"would","start":1912420,"end":1912620,"confidence":0.94873047,"speaker":"A"},{"text":"you","start":1912620,"end":1912900,"confidence":0.99902344,"speaker":"A"},{"text":"like,","start":1913060,"end":1913420,"confidence":0.9946289,"speaker":"A"},{"text":"how","start":1913420,"end":1913660,"confidence":0.9995117,"speaker":"A"},{"text":"do","start":1913660,"end":1913820,"confidence":0.99658203,"speaker":"A"},{"text":"you","start":1913820,"end":1914020,"confidence":0.9995117,"speaker":"A"},{"text":"do","start":1914020,"end":1914340,"confidence":0.99902344,"speaker":"A"},{"text":"adjacent","start":1914820,"end":1915620,"confidence":0.7462891,"speaker":"A"},{"text":"object","start":1915780,"end":1916220,"confidence":0.82470703,"speaker":"A"},{"text":"like","start":1916220,"end":1916460,"confidence":0.99902344,"speaker":"A"},{"text":"this?","start":1916460,"end":1916620,"confidence":0.99902344,"speaker":"A"},{"text":"How","start":1916620,"end":1916780,"confidence":0.9975586,"speaker":"A"},{"text":"would","start":1916780,"end":1916940,"confidence":0.99560547,"speaker":"A"},{"text":"you","start":1916940,"end":1917100,"confidence":0.9980469,"speaker":"A"},{"text":"even","start":1917100,"end":1917300,"confidence":0.9995117,"speaker":"A"},{"text":"represent","start":1917300,"end":1917620,"confidence":0.99853516,"speaker":"A"},{"text":"this","start":1917620,"end":1917900,"confidence":0.8857422,"speaker":"A"},{"text":"in","start":1917900,"end":1918060,"confidence":0.9404297,"speaker":"A"},{"text":"Swift?","start":1918060,"end":1918380,"confidence":0.9929199,"speaker":"A"},{"text":"Because","start":1918380,"end":1918580,"confidence":0.99902344,"speaker":"A"},{"text":"you","start":1918580,"end":1918740,"confidence":0.9995117,"speaker":"A"},{"text":"don't","start":1918740,"end":1918900,"confidence":0.99934894,"speaker":"A"},{"text":"know","start":1918900,"end":1918980,"confidence":0.99902344,"speaker":"A"},{"text":"what","start":1918980,"end":1919100,"confidence":0.9970703,"speaker":"A"},{"text":"type","start":1919100,"end":1919300,"confidence":0.9980469,"speaker":"A"},{"text":"you're","start":1919300,"end":1919460,"confidence":0.99820966,"speaker":"A"},{"text":"going","start":1919460,"end":1919540,"confidence":0.72802734,"speaker":"A"},{"text":"to","start":1919540,"end":1919620,"confidence":0.99902344,"speaker":"A"},{"text":"get.","start":1919620,"end":1919860,"confidence":0.9980469,"speaker":"A"},{"text":"So","start":1921350,"end":1921590,"confidence":0.9604492,"speaker":"A"},{"text":"like","start":1922790,"end":1923070,"confidence":0.99609375,"speaker":"A"},{"text":"I","start":1923070,"end":1923230,"confidence":0.9995117,"speaker":"A"},{"text":"said,","start":1923230,"end":1923390,"confidence":0.9995117,"speaker":"A"},{"text":"this","start":1923390,"end":1923550,"confidence":0.9995117,"speaker":"A"},{"text":"is","start":1923550,"end":1923710,"confidence":0.9975586,"speaker":"A"},{"text":"a","start":1923710,"end":1923830,"confidence":0.9980469,"speaker":"A"},{"text":"work","start":1923830,"end":1923950,"confidence":0.99853516,"speaker":"A"},{"text":"in","start":1923950,"end":1924110,"confidence":0.99902344,"speaker":"A"},{"text":"progress.","start":1924110,"end":1924510,"confidence":0.99975586,"speaker":"A"},{"text":"Sorry.","start":1924510,"end":1924950,"confidence":0.9889323,"speaker":"A"},{"text":"So","start":1925830,"end":1926150,"confidence":0.94628906,"speaker":"A"},{"text":"what","start":1926150,"end":1926350,"confidence":0.99609375,"speaker":"A"},{"text":"I","start":1926350,"end":1926550,"confidence":0.99853516,"speaker":"A"},{"text":"do,","start":1926550,"end":1926870,"confidence":0.99902344,"speaker":"A"},{"text":"I","start":1927190,"end":1927430,"confidence":0.99853516,"speaker":"A"},{"text":"don't","start":1927430,"end":1927590,"confidence":0.9785156,"speaker":"A"},{"text":"know","start":1927590,"end":1927670,"confidence":0.9975586,"speaker":"A"},{"text":"how","start":1927670,"end":1927790,"confidence":0.99902344,"speaker":"A"},{"text":"much","start":1927790,"end":1927950,"confidence":0.99902344,"speaker":"A"},{"text":"you","start":1927950,"end":1928110,"confidence":0.99853516,"speaker":"A"},{"text":"can","start":1928110,"end":1928270,"confidence":0.7426758,"speaker":"A"},{"text":"see","start":1928270,"end":1928430,"confidence":0.9995117,"speaker":"A"},{"text":"this.","start":1928430,"end":1928710,"confidence":0.9951172,"speaker":"A"},{"text":"I'm","start":1929110,"end":1929430,"confidence":0.99886066,"speaker":"A"},{"text":"going","start":1929430,"end":1929550,"confidence":0.71240234,"speaker":"A"},{"text":"to","start":1929550,"end":1929710,"confidence":0.99902344,"speaker":"A"},{"text":"actually","start":1929710,"end":1929910,"confidence":0.9975586,"speaker":"A"},{"text":"move","start":1929910,"end":1930150,"confidence":0.9995117,"speaker":"A"},{"text":"over","start":1930150,"end":1930430,"confidence":0.9995117,"speaker":"A"},{"text":"to","start":1930430,"end":1930790,"confidence":0.99853516,"speaker":"A"},{"text":"my","start":1932470,"end":1932870,"confidence":0.99902344,"speaker":"A"},{"text":"documentation","start":1932950,"end":1933910,"confidence":0.99990237,"speaker":"A"},{"text":"here","start":1933910,"end":1934310,"confidence":0.99609375,"speaker":"A"},{"text":"at","start":1935270,"end":1935550,"confidence":0.9951172,"speaker":"A"},{"text":"this","start":1935550,"end":1935710,"confidence":1,"speaker":"A"},{"text":"point.","start":1935710,"end":1935990,"confidence":0.9995117,"speaker":"A"},{"text":"So","start":1936150,"end":1936550,"confidence":0.9145508,"speaker":"A"},{"text":"how","start":1938310,"end":1938590,"confidence":0.99853516,"speaker":"A"},{"text":"are","start":1938590,"end":1938710,"confidence":0.9394531,"speaker":"A"},{"text":"we","start":1938710,"end":1938830,"confidence":0.42895508,"speaker":"A"},{"text":"doing","start":1938830,"end":1938990,"confidence":0.9980469,"speaker":"A"},{"text":"on","start":1938990,"end":1939190,"confidence":0.99853516,"speaker":"A"},{"text":"time?","start":1939190,"end":1939510,"confidence":0.9995117,"speaker":"A"},{"text":"We","start":1939510,"end":1939790,"confidence":0.7001953,"speaker":"A"},{"text":"good?","start":1939790,"end":1940070,"confidence":0.98876953,"speaker":"A"},{"text":"Yeah,","start":1942550,"end":1942870,"confidence":0.9842122,"speaker":"B"},{"text":"I","start":1942870,"end":1942990,"confidence":0.59228516,"speaker":"B"},{"text":"think,","start":1942990,"end":1943190,"confidence":0.9770508,"speaker":"B"},{"text":"I","start":1943190,"end":1943350,"confidence":0.96240234,"speaker":"B"},{"text":"think","start":1943350,"end":1943470,"confidence":0.9975586,"speaker":"B"},{"text":"we're","start":1943470,"end":1943670,"confidence":0.99902344,"speaker":"B"},{"text":"doing","start":1943670,"end":1943790,"confidence":0.9980469,"speaker":"B"},{"text":"good.","start":1943790,"end":1944070,"confidence":0.9951172,"speaker":"B"},{"text":"Okay,","start":1944870,"end":1945310,"confidence":0.94189453,"speaker":"A"},{"text":"cool.","start":1945310,"end":1945590,"confidence":0.99780273,"speaker":"A"},{"text":"Any,","start":1945590,"end":1945910,"confidence":0.90234375,"speaker":"A"},{"text":"do","start":1946560,"end":1946640,"confidence":0.70996094,"speaker":"A"},{"text":"you","start":1946640,"end":1946760,"confidence":0.9946289,"speaker":"A"},{"text":"want","start":1946760,"end":1946880,"confidence":0.9321289,"speaker":"A"},{"text":"to","start":1946880,"end":1946960,"confidence":0.9980469,"speaker":"A"},{"text":"ask","start":1946960,"end":1947120,"confidence":0.9995117,"speaker":"A"},{"text":"questions?","start":1947120,"end":1947680,"confidence":0.99975586,"speaker":"A"},{"text":"I","start":1949680,"end":1949960,"confidence":0.9975586,"speaker":"B"},{"text":"don't","start":1949960,"end":1950240,"confidence":0.9991862,"speaker":"B"},{"text":"have","start":1950240,"end":1950480,"confidence":0.9995117,"speaker":"B"},{"text":"anything","start":1950480,"end":1950960,"confidence":0.99975586,"speaker":"B"},{"text":"right","start":1951440,"end":1951800,"confidence":0.99902344,"speaker":"B"},{"text":"now.","start":1951800,"end":1952160,"confidence":0.99853516,"speaker":"B"},{"text":"Same","start":1953760,"end":1954160,"confidence":0.98291016,"speaker":"C"},{"text":"nothing","start":1954240,"end":1954600,"confidence":0.99975586,"speaker":"C"},{"text":"right","start":1954600,"end":1954800,"confidence":0.9995117,"speaker":"C"},{"text":"now.","start":1954800,"end":1955040,"confidence":0.9995117,"speaker":"C"},{"text":"But","start":1955040,"end":1955240,"confidence":0.9980469,"speaker":"C"},{"text":"this","start":1955240,"end":1955440,"confidence":0.99853516,"speaker":"C"},{"text":"seems","start":1955440,"end":1955880,"confidence":0.99975586,"speaker":"C"},{"text":"applicable","start":1955880,"end":1956560,"confidence":0.99975586,"speaker":"C"},{"text":"to","start":1956560,"end":1956960,"confidence":0.9995117,"speaker":"C"},{"text":"things","start":1957280,"end":1957600,"confidence":1,"speaker":"C"},{"text":"I'll","start":1957600,"end":1957880,"confidence":0.98779297,"speaker":"C"},{"text":"be","start":1957880,"end":1958000,"confidence":0.9995117,"speaker":"C"},{"text":"doing","start":1958000,"end":1958200,"confidence":0.9995117,"speaker":"C"},{"text":"coming","start":1958200,"end":1958480,"confidence":0.99853516,"speaker":"C"},{"text":"up.","start":1958480,"end":1958800,"confidence":0.99609375,"speaker":"C"},{"text":"Okay,","start":1959360,"end":1960000,"confidence":0.88964844,"speaker":"A"},{"text":"cool.","start":1960000,"end":1960480,"confidence":0.99902344,"speaker":"A"},{"text":"So","start":1963200,"end":1963600,"confidence":0.8515625,"speaker":"A"},{"text":"we","start":1964480,"end":1964760,"confidence":0.9838867,"speaker":"A"},{"text":"have","start":1964760,"end":1964960,"confidence":0.59765625,"speaker":"A"},{"text":"set","start":1964960,"end":1965200,"confidence":0.99902344,"speaker":"A"},{"text":"up","start":1965200,"end":1965520,"confidence":0.9716797,"speaker":"A"},{"text":"in","start":1965920,"end":1966280,"confidence":0.85595703,"speaker":"A"},{"text":"the","start":1966280,"end":1966640,"confidence":0.98291016,"speaker":"A"},{"text":"open.","start":1966800,"end":1967200,"confidence":0.9916992,"speaker":"A"},{"text":"So","start":1967200,"end":1967440,"confidence":0.93896484,"speaker":"A"},{"text":"we","start":1967440,"end":1967520,"confidence":0.9980469,"speaker":"A"},{"text":"have","start":1967520,"end":1967640,"confidence":0.99902344,"speaker":"A"},{"text":"an","start":1967640,"end":1967760,"confidence":0.9116211,"speaker":"A"},{"text":"open","start":1967760,"end":1967960,"confidence":0.99853516,"speaker":"A"},{"text":"API","start":1967960,"end":1968480,"confidence":0.9958496,"speaker":"A"},{"text":"YAML","start":1968480,"end":1968920,"confidence":0.9547526,"speaker":"A"},{"text":"file","start":1968920,"end":1969360,"confidence":0.99731445,"speaker":"A"},{"text":"that","start":1969760,"end":1970040,"confidence":0.9980469,"speaker":"A"},{"text":"you","start":1970040,"end":1970240,"confidence":0.99902344,"speaker":"A"},{"text":"can","start":1970240,"end":1970400,"confidence":0.99853516,"speaker":"A"},{"text":"pull","start":1970400,"end":1970560,"confidence":0.99975586,"speaker":"A"},{"text":"up","start":1970560,"end":1970680,"confidence":0.9995117,"speaker":"A"},{"text":"in","start":1970680,"end":1970880,"confidence":0.9970703,"speaker":"A"},{"text":"Miskit,","start":1970880,"end":1971520,"confidence":0.98657227,"speaker":"A"},{"text":"which","start":1972250,"end":1972370,"confidence":0.9975586,"speaker":"A"},{"text":"is","start":1972370,"end":1972650,"confidence":0.99902344,"speaker":"A"},{"text":"basically","start":1972730,"end":1973370,"confidence":0.99975586,"speaker":"A"},{"text":"every","start":1973370,"end":1973770,"confidence":0.99365234,"speaker":"A"},{"text":"like","start":1973770,"end":1974170,"confidence":0.98828125,"speaker":"A"},{"text":"the","start":1975050,"end":1975370,"confidence":0.99902344,"speaker":"A"},{"text":"documentation","start":1975370,"end":1976170,"confidence":0.99912107,"speaker":"A"},{"text":"converted","start":1976330,"end":1977010,"confidence":0.9996745,"speaker":"A"},{"text":"to","start":1977010,"end":1977210,"confidence":0.9975586,"speaker":"A"},{"text":"YAML.","start":1977210,"end":1977850,"confidence":0.71435547,"speaker":"A"},{"text":"And","start":1978410,"end":1978770,"confidence":0.99072266,"speaker":"A"},{"text":"so","start":1978770,"end":1978970,"confidence":1,"speaker":"A"},{"text":"what","start":1978970,"end":1979090,"confidence":0.9995117,"speaker":"A"},{"text":"we","start":1979090,"end":1979290,"confidence":1,"speaker":"A"},{"text":"do","start":1979290,"end":1979570,"confidence":0.9980469,"speaker":"A"},{"text":"is","start":1979570,"end":1979930,"confidence":0.6928711,"speaker":"A"},{"text":"you","start":1980090,"end":1980410,"confidence":1,"speaker":"A"},{"text":"can","start":1980410,"end":1980690,"confidence":1,"speaker":"A"},{"text":"set","start":1980690,"end":1980930,"confidence":0.9995117,"speaker":"A"},{"text":"up","start":1980930,"end":1981210,"confidence":0.9975586,"speaker":"A"},{"text":"in","start":1982490,"end":1982770,"confidence":0.98095703,"speaker":"A"},{"text":"the","start":1982770,"end":1982930,"confidence":0.9951172,"speaker":"A"},{"text":"YAML","start":1982930,"end":1983250,"confidence":0.8038737,"speaker":"A"},{"text":"the","start":1983250,"end":1983410,"confidence":0.97753906,"speaker":"A"},{"text":"field","start":1983410,"end":1983690,"confidence":0.9980469,"speaker":"A"},{"text":"value","start":1983770,"end":1984130,"confidence":1,"speaker":"A"},{"text":"requests","start":1984130,"end":1984690,"confidence":0.8439128,"speaker":"A"},{"text":"and","start":1984690,"end":1984810,"confidence":0.9970703,"speaker":"A"},{"text":"they","start":1984810,"end":1984930,"confidence":1,"speaker":"A"},{"text":"have","start":1984930,"end":1985090,"confidence":1,"speaker":"A"},{"text":"an","start":1985090,"end":1985290,"confidence":0.9633789,"speaker":"A"},{"text":"enum","start":1985290,"end":1985770,"confidence":0.8808594,"speaker":"A"},{"text":"type","start":1985770,"end":1986090,"confidence":0.8652344,"speaker":"A"},{"text":"essentially","start":1986090,"end":1986650,"confidence":0.94311523,"speaker":"A"},{"text":"for,","start":1987930,"end":1988330,"confidence":0.96875,"speaker":"A"},{"text":"for","start":1992090,"end":1992450,"confidence":0.9995117,"speaker":"A"},{"text":"open","start":1992450,"end":1992810,"confidence":0.9995117,"speaker":"A"},{"text":"API.","start":1992970,"end":1993610,"confidence":0.9975586,"speaker":"A"},{"text":"So","start":1993690,"end":1994090,"confidence":0.98583984,"speaker":"A"},{"text":"and","start":1994970,"end":1995250,"confidence":0.9350586,"speaker":"A"},{"text":"then,","start":1995250,"end":1995490,"confidence":0.39233398,"speaker":"A"},{"text":"so","start":1995490,"end":1995770,"confidence":0.9975586,"speaker":"A"},{"text":"this","start":1995770,"end":1996010,"confidence":0.99902344,"speaker":"A"},{"text":"has,","start":1996010,"end":1996330,"confidence":0.99853516,"speaker":"A"},{"text":"you","start":1996330,"end":1996570,"confidence":0.6645508,"speaker":"A"},{"text":"know,","start":1996570,"end":1996690,"confidence":0.97998047,"speaker":"A"},{"text":"it","start":1996690,"end":1996810,"confidence":0.9975586,"speaker":"A"},{"text":"could","start":1996810,"end":1996930,"confidence":0.9838867,"speaker":"A"},{"text":"be","start":1996930,"end":1997090,"confidence":1,"speaker":"A"},{"text":"one","start":1997090,"end":1997210,"confidence":0.99853516,"speaker":"A"},{"text":"of","start":1997210,"end":1997410,"confidence":0.99902344,"speaker":"A"},{"text":"either","start":1997410,"end":1997770,"confidence":0.9968262,"speaker":"A"},{"text":"any","start":1997770,"end":1998010,"confidence":0.9995117,"speaker":"A"},{"text":"of","start":1998010,"end":1998170,"confidence":1,"speaker":"A"},{"text":"these","start":1998170,"end":1998370,"confidence":0.99902344,"speaker":"A"},{"text":"types","start":1998370,"end":1998810,"confidence":0.9453125,"speaker":"A"},{"text":"of.","start":1998860,"end":1999020,"confidence":0.5004883,"speaker":"A"},{"text":"And","start":2000050,"end":2000210,"confidence":0.97216797,"speaker":"A"},{"text":"then","start":2000210,"end":2000530,"confidence":0.9995117,"speaker":"A"},{"text":"there's","start":2000850,"end":2001210,"confidence":0.99560547,"speaker":"A"},{"text":"an","start":2001210,"end":2001370,"confidence":0.76220703,"speaker":"A"},{"text":"enum","start":2001370,"end":2001850,"confidence":0.92211914,"speaker":"A"},{"text":"in","start":2001850,"end":2002090,"confidence":0.9995117,"speaker":"A"},{"text":"case","start":2002090,"end":2002290,"confidence":1,"speaker":"A"},{"text":"you","start":2002290,"end":2002530,"confidence":0.9995117,"speaker":"A"},{"text":"have","start":2002530,"end":2002730,"confidence":1,"speaker":"A"},{"text":"a","start":2002730,"end":2002890,"confidence":0.99902344,"speaker":"A"},{"text":"list.","start":2002890,"end":2003170,"confidence":0.9995117,"speaker":"A"},{"text":"So","start":2004050,"end":2004450,"confidence":0.99560547,"speaker":"A"},{"text":"if","start":2005250,"end":2005570,"confidence":0.9980469,"speaker":"A"},{"text":"you","start":2005570,"end":2005770,"confidence":1,"speaker":"A"},{"text":"have","start":2005770,"end":2005970,"confidence":1,"speaker":"A"},{"text":"a","start":2005970,"end":2006210,"confidence":0.99902344,"speaker":"A"},{"text":"list","start":2006210,"end":2006530,"confidence":0.9995117,"speaker":"A"},{"text":"value","start":2006850,"end":2007250,"confidence":0.9995117,"speaker":"A"},{"text":"type","start":2007330,"end":2007890,"confidence":0.99780273,"speaker":"A"},{"text":"there","start":2008530,"end":2008850,"confidence":1,"speaker":"A"},{"text":"is","start":2008850,"end":2009090,"confidence":1,"speaker":"A"},{"text":"an","start":2009090,"end":2009290,"confidence":0.9995117,"speaker":"A"},{"text":"extra","start":2009290,"end":2009690,"confidence":0.99975586,"speaker":"A"},{"text":"property","start":2009690,"end":2010290,"confidence":0.9995117,"speaker":"A"},{"text":"called","start":2010290,"end":2010690,"confidence":0.9995117,"speaker":"A"},{"text":"type","start":2011010,"end":2011450,"confidence":0.81103516,"speaker":"A"},{"text":"and","start":2011450,"end":2011690,"confidence":0.9951172,"speaker":"A"},{"text":"then","start":2011690,"end":2011850,"confidence":0.99365234,"speaker":"A"},{"text":"that","start":2011850,"end":2012010,"confidence":0.9995117,"speaker":"A"},{"text":"will","start":2012010,"end":2012210,"confidence":0.9995117,"speaker":"A"},{"text":"tell","start":2012210,"end":2012410,"confidence":1,"speaker":"A"},{"text":"you","start":2012410,"end":2012570,"confidence":1,"speaker":"A"},{"text":"what","start":2012570,"end":2012810,"confidence":0.59277344,"speaker":"A"},{"text":"type","start":2012810,"end":2013250,"confidence":0.8652344,"speaker":"A"},{"text":"the.","start":2013410,"end":2013810,"confidence":0.98876953,"speaker":"A"},{"text":"The","start":2014450,"end":2014730,"confidence":0.99853516,"speaker":"A"},{"text":"list","start":2014730,"end":2015010,"confidence":0.9995117,"speaker":"A"},{"text":"is.","start":2015010,"end":2015329,"confidence":0.9995117,"speaker":"A"},{"text":"And","start":2015329,"end":2015570,"confidence":0.99365234,"speaker":"A"},{"text":"it's","start":2015570,"end":2016050,"confidence":0.99397784,"speaker":"A"},{"text":"homo","start":2016530,"end":2017250,"confidence":0.8297526,"speaker":"A"},{"text":"homomorphic.","start":2017250,"end":2018450,"confidence":0.99763995,"speaker":"A"},{"text":"It's","start":2018690,"end":2019050,"confidence":0.9720052,"speaker":"A"},{"text":"all","start":2019050,"end":2019210,"confidence":0.99560547,"speaker":"A"},{"text":"the","start":2019210,"end":2019330,"confidence":0.9995117,"speaker":"A"},{"text":"same","start":2019330,"end":2019570,"confidence":0.99902344,"speaker":"A"},{"text":"list","start":2019890,"end":2020210,"confidence":0.97314453,"speaker":"A"},{"text":"type.","start":2020210,"end":2020490,"confidence":0.9848633,"speaker":"A"},{"text":"You","start":2020490,"end":2020610,"confidence":0.9995117,"speaker":"A"},{"text":"can't","start":2020610,"end":2020810,"confidence":0.98567706,"speaker":"A"},{"text":"have","start":2020810,"end":2021010,"confidence":1,"speaker":"A"},{"text":"lists","start":2021010,"end":2021330,"confidence":0.9987793,"speaker":"A"},{"text":"of","start":2021330,"end":2021450,"confidence":0.9995117,"speaker":"A"},{"text":"different","start":2021450,"end":2021690,"confidence":1,"speaker":"A"},{"text":"types.","start":2021690,"end":2022210,"confidence":0.92578125,"speaker":"A"},{"text":"And","start":2024050,"end":2024450,"confidence":0.95751953,"speaker":"A"},{"text":"then","start":2024610,"end":2025010,"confidence":0.9038086,"speaker":"A"},{"text":"we","start":2026030,"end":2026190,"confidence":0.9941406,"speaker":"A"},{"text":"have","start":2026190,"end":2026470,"confidence":0.9995117,"speaker":"A"},{"text":"here","start":2026470,"end":2026830,"confidence":0.99902344,"speaker":"A"},{"text":"again","start":2028830,"end":2029230,"confidence":0.99853516,"speaker":"A"},{"text":"field","start":2029230,"end":2029590,"confidence":0.9404297,"speaker":"A"},{"text":"value.","start":2029590,"end":2029950,"confidence":0.99902344,"speaker":"A"},{"text":"Sometimes","start":2031390,"end":2031910,"confidence":0.99886066,"speaker":"A"},{"text":"the","start":2031910,"end":2032070,"confidence":0.98876953,"speaker":"A"},{"text":"type","start":2032070,"end":2032310,"confidence":0.9086914,"speaker":"A"},{"text":"is","start":2032310,"end":2032470,"confidence":0.99853516,"speaker":"A"},{"text":"available,","start":2032470,"end":2032750,"confidence":0.9995117,"speaker":"A"},{"text":"sometimes","start":2032910,"end":2033430,"confidence":0.9996745,"speaker":"A"},{"text":"it's","start":2033430,"end":2033750,"confidence":0.99886066,"speaker":"A"},{"text":"not.","start":2033750,"end":2034030,"confidence":0.9995117,"speaker":"A"},{"text":"But","start":2034590,"end":2034910,"confidence":0.99658203,"speaker":"A"},{"text":"basically","start":2034910,"end":2035390,"confidence":0.99975586,"speaker":"A"},{"text":"we","start":2035390,"end":2035670,"confidence":0.9995117,"speaker":"A"},{"text":"have","start":2035670,"end":2035910,"confidence":0.9995117,"speaker":"A"},{"text":"all","start":2035910,"end":2036150,"confidence":1,"speaker":"A"},{"text":"the","start":2036150,"end":2036310,"confidence":0.9995117,"speaker":"A"},{"text":"different","start":2036310,"end":2036590,"confidence":0.9995117,"speaker":"A"},{"text":"value","start":2036750,"end":2037150,"confidence":0.99902344,"speaker":"A"},{"text":"types","start":2037230,"end":2037710,"confidence":0.99975586,"speaker":"A"},{"text":"available","start":2037710,"end":2038030,"confidence":0.99902344,"speaker":"A"},{"text":"to","start":2038190,"end":2038470,"confidence":1,"speaker":"A"},{"text":"us","start":2038470,"end":2038750,"confidence":1,"speaker":"A"},{"text":"in","start":2038830,"end":2039110,"confidence":0.97802734,"speaker":"A"},{"text":"a","start":2039110,"end":2039270,"confidence":0.96728516,"speaker":"A"},{"text":"CK","start":2039270,"end":2039630,"confidence":0.9001465,"speaker":"A"},{"text":"value.","start":2039630,"end":2039950,"confidence":0.9091797,"speaker":"A"},{"text":"And","start":2041950,"end":2042230,"confidence":0.9848633,"speaker":"A"},{"text":"then","start":2042230,"end":2042510,"confidence":0.99902344,"speaker":"A"},{"text":"this","start":2042990,"end":2043310,"confidence":0.99853516,"speaker":"A"},{"text":"is.","start":2043310,"end":2043550,"confidence":0.99902344,"speaker":"A"},{"text":"Then","start":2043550,"end":2043870,"confidence":0.9848633,"speaker":"A"},{"text":"the","start":2044110,"end":2044430,"confidence":0.98828125,"speaker":"A"},{"text":"Open","start":2044430,"end":2044750,"confidence":0.9946289,"speaker":"A"},{"text":"API","start":2045150,"end":2045670,"confidence":0.99780273,"speaker":"A"},{"text":"generator","start":2045670,"end":2046190,"confidence":0.97143555,"speaker":"A"},{"text":"essentially","start":2046190,"end":2046870,"confidence":0.99902344,"speaker":"A"},{"text":"builds","start":2046870,"end":2047310,"confidence":0.9782715,"speaker":"A"},{"text":"this","start":2047310,"end":2047470,"confidence":0.9926758,"speaker":"A"},{"text":"for","start":2047470,"end":2047670,"confidence":0.9838867,"speaker":"A"},{"text":"me","start":2047670,"end":2047950,"confidence":0.99853516,"speaker":"A"},{"text":"which","start":2048510,"end":2048830,"confidence":0.9980469,"speaker":"A"},{"text":"is.","start":2048830,"end":2049150,"confidence":0.9873047,"speaker":"A"},{"text":"Has","start":2049710,"end":2049990,"confidence":0.9980469,"speaker":"A"},{"text":"an","start":2049990,"end":2050150,"confidence":0.47924805,"speaker":"A"},{"text":"enum","start":2050150,"end":2050670,"confidence":0.7680664,"speaker":"A"},{"text":"and","start":2050830,"end":2051110,"confidence":0.9902344,"speaker":"A"},{"text":"a","start":2051110,"end":2051270,"confidence":0.9863281,"speaker":"A"},{"text":"struck","start":2051270,"end":2051510,"confidence":0.7644043,"speaker":"A"},{"text":"for","start":2051510,"end":2051670,"confidence":0.5751953,"speaker":"A"},{"text":"field","start":2051670,"end":2051950,"confidence":0.7363281,"speaker":"A"},{"text":"field","start":2052110,"end":2052510,"confidence":1,"speaker":"A"},{"text":"value","start":2052670,"end":2053070,"confidence":0.99902344,"speaker":"A"},{"text":"request","start":2053070,"end":2053630,"confidence":0.7783203,"speaker":"A"},{"text":"and","start":2055329,"end":2055449,"confidence":0.9321289,"speaker":"A"},{"text":"then","start":2055449,"end":2055609,"confidence":0.9946289,"speaker":"A"},{"text":"it","start":2055609,"end":2055769,"confidence":1,"speaker":"A"},{"text":"does","start":2055769,"end":2055929,"confidence":0.9995117,"speaker":"A"},{"text":"all","start":2055929,"end":2056089,"confidence":0.9941406,"speaker":"A"},{"text":"the","start":2056089,"end":2056249,"confidence":0.9946289,"speaker":"A"},{"text":"decoding","start":2056249,"end":2056769,"confidence":0.99886066,"speaker":"A"},{"text":"for","start":2056769,"end":2056969,"confidence":0.99902344,"speaker":"A"},{"text":"me.","start":2056969,"end":2057249,"confidence":1,"speaker":"A"},{"text":"Thankfully","start":2057249,"end":2057849,"confidence":0.99523926,"speaker":"A"},{"text":"I","start":2057849,"end":2058089,"confidence":0.99560547,"speaker":"A"},{"text":"didn't","start":2058089,"end":2058289,"confidence":0.95670575,"speaker":"A"},{"text":"have","start":2058289,"end":2058369,"confidence":0.99902344,"speaker":"A"},{"text":"to","start":2058369,"end":2058449,"confidence":0.9980469,"speaker":"A"},{"text":"do","start":2058449,"end":2058569,"confidence":0.91845703,"speaker":"A"},{"text":"any","start":2058569,"end":2058769,"confidence":1,"speaker":"A"},{"text":"of","start":2058769,"end":2058929,"confidence":0.9995117,"speaker":"A"},{"text":"it.","start":2058929,"end":2059169,"confidence":0.9975586,"speaker":"A"},{"text":"And","start":2063089,"end":2063369,"confidence":0.97021484,"speaker":"A"},{"text":"then","start":2063369,"end":2063649,"confidence":0.99658203,"speaker":"A"},{"text":"yeah,","start":2065409,"end":2065809,"confidence":0.94091797,"speaker":"A"},{"text":"I","start":2065809,"end":2066009,"confidence":0.99902344,"speaker":"A"},{"text":"just","start":2066009,"end":2066169,"confidence":0.99902344,"speaker":"A"},{"text":"wanted","start":2066169,"end":2066409,"confidence":0.99780273,"speaker":"A"},{"text":"to","start":2066409,"end":2066569,"confidence":0.99902344,"speaker":"A"},{"text":"cover","start":2066569,"end":2066769,"confidence":1,"speaker":"A"},{"text":"that","start":2066769,"end":2067009,"confidence":0.9995117,"speaker":"A"},{"text":"piece","start":2067009,"end":2067409,"confidence":0.9667969,"speaker":"A"},{"text":"where","start":2067569,"end":2067929,"confidence":0.9995117,"speaker":"A"},{"text":"we","start":2067929,"end":2068249,"confidence":0.9995117,"speaker":"A"},{"text":"show","start":2068249,"end":2068609,"confidence":0.99902344,"speaker":"A"},{"text":"how","start":2068929,"end":2069249,"confidence":0.9995117,"speaker":"A"},{"text":"we","start":2069249,"end":2069449,"confidence":1,"speaker":"A"},{"text":"deal","start":2069449,"end":2069609,"confidence":1,"speaker":"A"},{"text":"with","start":2069609,"end":2069888,"confidence":0.9995117,"speaker":"A"},{"text":"these","start":2069888,"end":2070209,"confidence":0.99072266,"speaker":"A"},{"text":"kind","start":2070209,"end":2070369,"confidence":0.98876953,"speaker":"A"},{"text":"of","start":2070369,"end":2070529,"confidence":0.5283203,"speaker":"A"},{"text":"like","start":2070529,"end":2070729,"confidence":0.984375,"speaker":"A"},{"text":"polymorphic","start":2070729,"end":2071969,"confidence":0.9777832,"speaker":"A"},{"text":"types","start":2071969,"end":2072529,"confidence":0.76416016,"speaker":"A"},{"text":"and","start":2073249,"end":2073529,"confidence":0.99658203,"speaker":"A"},{"text":"how","start":2073529,"end":2073729,"confidence":0.9995117,"speaker":"A"},{"text":"those","start":2073729,"end":2073969,"confidence":0.99902344,"speaker":"A"},{"text":"work.","start":2073969,"end":2074289,"confidence":0.99853516,"speaker":"A"},{"text":"The","start":2075329,"end":2075569,"confidence":0.9746094,"speaker":"A"},{"text":"next","start":2075569,"end":2075729,"confidence":0.9902344,"speaker":"A"},{"text":"thing","start":2075729,"end":2075889,"confidence":0.9692383,"speaker":"A"},{"text":"I","start":2075889,"end":2075969,"confidence":0.89208984,"speaker":"A"},{"text":"want","start":2075969,"end":2076089,"confidence":0.79052734,"speaker":"A"},{"text":"to","start":2076089,"end":2076209,"confidence":0.99902344,"speaker":"A"},{"text":"cover","start":2076209,"end":2076409,"confidence":0.9995117,"speaker":"A"},{"text":"is","start":2076409,"end":2076689,"confidence":0.99853516,"speaker":"A"},{"text":"error","start":2076689,"end":2077009,"confidence":0.914917,"speaker":"A"},{"text":"handling.","start":2077009,"end":2077489,"confidence":0.99902344,"speaker":"A"},{"text":"So","start":2079249,"end":2079529,"confidence":0.99121094,"speaker":"A"},{"text":"if","start":2079529,"end":2079729,"confidence":0.6791992,"speaker":"A"},{"text":"you","start":2079729,"end":2079929,"confidence":1,"speaker":"A"},{"text":"look","start":2079929,"end":2080049,"confidence":1,"speaker":"A"},{"text":"at","start":2080049,"end":2080169,"confidence":1,"speaker":"A"},{"text":"the","start":2080169,"end":2080289,"confidence":1,"speaker":"A"},{"text":"documentation","start":2080289,"end":2081009,"confidence":0.9964844,"speaker":"A"},{"text":"gives","start":2081569,"end":2081969,"confidence":0.9904785,"speaker":"A"},{"text":"you.","start":2081969,"end":2082209,"confidence":0.99658203,"speaker":"A"},{"text":"If","start":2083390,"end":2083510,"confidence":0.98876953,"speaker":"A"},{"text":"you","start":2083510,"end":2083630,"confidence":0.9975586,"speaker":"A"},{"text":"get","start":2083630,"end":2083750,"confidence":0.97509766,"speaker":"A"},{"text":"an","start":2083750,"end":2083910,"confidence":0.9604492,"speaker":"A"},{"text":"error","start":2083910,"end":2084270,"confidence":0.8522949,"speaker":"A"},{"text":"we","start":2085150,"end":2085430,"confidence":0.99121094,"speaker":"A"},{"text":"get","start":2085430,"end":2085630,"confidence":0.71777344,"speaker":"A"},{"text":"something","start":2085630,"end":2085870,"confidence":0.9995117,"speaker":"A"},{"text":"like","start":2085870,"end":2086070,"confidence":0.9995117,"speaker":"A"},{"text":"this","start":2086070,"end":2086350,"confidence":0.99902344,"speaker":"A"},{"text":"and","start":2088030,"end":2088350,"confidence":0.9238281,"speaker":"A"},{"text":"then","start":2088350,"end":2088630,"confidence":0.9921875,"speaker":"A"},{"text":"that","start":2088630,"end":2088910,"confidence":0.90283203,"speaker":"A"},{"text":"will","start":2088910,"end":2089150,"confidence":0.7714844,"speaker":"A"},{"text":"show","start":2089150,"end":2089350,"confidence":0.9995117,"speaker":"A"},{"text":"you","start":2089350,"end":2089630,"confidence":0.99658203,"speaker":"A"},{"text":"in","start":2089870,"end":2090150,"confidence":0.7524414,"speaker":"A"},{"text":"the.","start":2090150,"end":2090350,"confidence":0.80615234,"speaker":"A"},{"text":"In","start":2090350,"end":2090590,"confidence":0.98876953,"speaker":"A"},{"text":"the","start":2090590,"end":2090750,"confidence":0.9995117,"speaker":"A"},{"text":"table","start":2090750,"end":2091070,"confidence":0.9995117,"speaker":"A"},{"text":"actually","start":2091070,"end":2091390,"confidence":0.99853516,"speaker":"A"},{"text":"shows","start":2091390,"end":2091710,"confidence":0.99975586,"speaker":"A"},{"text":"you","start":2091710,"end":2091830,"confidence":0.9995117,"speaker":"A"},{"text":"what","start":2091830,"end":2092030,"confidence":0.9995117,"speaker":"A"},{"text":"each","start":2092030,"end":2092350,"confidence":0.9995117,"speaker":"A"},{"text":"error","start":2092830,"end":2093270,"confidence":0.87854004,"speaker":"A"},{"text":"means.","start":2093270,"end":2093630,"confidence":0.99853516,"speaker":"A"},{"text":"So","start":2094830,"end":2095230,"confidence":0.9707031,"speaker":"A"},{"text":"again","start":2095230,"end":2095630,"confidence":0.9995117,"speaker":"A"},{"text":"we","start":2095710,"end":2095990,"confidence":1,"speaker":"A"},{"text":"do","start":2095990,"end":2096150,"confidence":0.9980469,"speaker":"A"},{"text":"like","start":2096150,"end":2096270,"confidence":0.9892578,"speaker":"A"},{"text":"an","start":2096270,"end":2096430,"confidence":0.9868164,"speaker":"A"},{"text":"enum","start":2096430,"end":2096990,"confidence":0.9489746,"speaker":"A"},{"text":"in","start":2097150,"end":2097470,"confidence":0.54541016,"speaker":"A"},{"text":"YAML.","start":2097470,"end":2098110,"confidence":0.94954425,"speaker":"A"},{"text":"It's","start":2098830,"end":2099190,"confidence":0.99853516,"speaker":"A"},{"text":"basically","start":2099190,"end":2099550,"confidence":0.99975586,"speaker":"A"},{"text":"a","start":2099550,"end":2099750,"confidence":0.9970703,"speaker":"A"},{"text":"string","start":2099750,"end":2100110,"confidence":0.99975586,"speaker":"A"},{"text":"and","start":2100110,"end":2100310,"confidence":0.99658203,"speaker":"A"},{"text":"then","start":2100310,"end":2100430,"confidence":0.9746094,"speaker":"A"},{"text":"we","start":2100430,"end":2100550,"confidence":0.9995117,"speaker":"A"},{"text":"have","start":2100550,"end":2100710,"confidence":0.9995117,"speaker":"A"},{"text":"everything","start":2100710,"end":2100910,"confidence":0.9995117,"speaker":"A"},{"text":"else","start":2100910,"end":2101190,"confidence":0.99975586,"speaker":"A"},{"text":"be","start":2101190,"end":2101350,"confidence":0.98046875,"speaker":"A"},{"text":"a","start":2101350,"end":2101510,"confidence":0.99853516,"speaker":"A"},{"text":"string.","start":2101510,"end":2101950,"confidence":0.99902344,"speaker":"A"},{"text":"And","start":2102590,"end":2102870,"confidence":0.96240234,"speaker":"A"},{"text":"then","start":2102870,"end":2103150,"confidence":0.99902344,"speaker":"A"},{"text":"the","start":2103310,"end":2103590,"confidence":0.9946289,"speaker":"A"},{"text":"open","start":2103590,"end":2103790,"confidence":0.9946289,"speaker":"A"},{"text":"API","start":2103790,"end":2104270,"confidence":0.95581055,"speaker":"A"},{"text":"generator","start":2104270,"end":2104790,"confidence":0.998291,"speaker":"A"},{"text":"will","start":2104790,"end":2105030,"confidence":0.9975586,"speaker":"A"},{"text":"automatically","start":2105030,"end":2105590,"confidence":0.8905029,"speaker":"A"},{"text":"generate","start":2105590,"end":2106110,"confidence":1,"speaker":"A"},{"text":"this","start":2106110,"end":2106430,"confidence":0.9970703,"speaker":"A"},{"text":"which","start":2107710,"end":2108110,"confidence":0.9975586,"speaker":"A"},{"text":"gives","start":2108110,"end":2108510,"confidence":0.9970703,"speaker":"A"},{"text":"us","start":2108510,"end":2108630,"confidence":0.99609375,"speaker":"A"},{"text":"the","start":2108630,"end":2108910,"confidence":0.53759766,"speaker":"A"},{"text":"server","start":2109500,"end":2109860,"confidence":0.9980469,"speaker":"A"},{"text":"error","start":2109860,"end":2110140,"confidence":0.986084,"speaker":"A"},{"text":"code","start":2110140,"end":2110500,"confidence":0.9977214,"speaker":"A"},{"text":"and","start":2110500,"end":2110740,"confidence":0.9145508,"speaker":"A"},{"text":"the","start":2110740,"end":2110980,"confidence":0.95751953,"speaker":"A"},{"text":"error","start":2110980,"end":2111220,"confidence":0.9855957,"speaker":"A"},{"text":"response.","start":2111220,"end":2111820,"confidence":0.89868164,"speaker":"A"},{"text":"It'll","start":2112380,"end":2112820,"confidence":0.9863281,"speaker":"A"},{"text":"also","start":2112820,"end":2113060,"confidence":1,"speaker":"A"},{"text":"do","start":2113060,"end":2113300,"confidence":1,"speaker":"A"},{"text":"all","start":2113300,"end":2113460,"confidence":1,"speaker":"A"},{"text":"this","start":2113460,"end":2113660,"confidence":0.61621094,"speaker":"A"},{"text":"stuff","start":2113660,"end":2113980,"confidence":1,"speaker":"A"},{"text":"here,","start":2113980,"end":2114260,"confidence":1,"speaker":"A"},{"text":"which","start":2114260,"end":2114580,"confidence":0.9399414,"speaker":"A"},{"text":"is","start":2114580,"end":2114820,"confidence":0.99658203,"speaker":"A"},{"text":"really","start":2114820,"end":2115060,"confidence":0.74316406,"speaker":"A"},{"text":"nice.","start":2115060,"end":2115500,"confidence":1,"speaker":"A"},{"text":"And","start":2117980,"end":2118260,"confidence":0.9970703,"speaker":"A"},{"text":"then","start":2118260,"end":2118540,"confidence":0.9995117,"speaker":"A"},{"text":"we've","start":2118620,"end":2119180,"confidence":0.9142253,"speaker":"A"},{"text":"then","start":2119180,"end":2119500,"confidence":0.953125,"speaker":"A"},{"text":"in","start":2119500,"end":2119700,"confidence":0.984375,"speaker":"A"},{"text":"our.","start":2119700,"end":2119980,"confidence":0.9980469,"speaker":"A"},{"text":"We've","start":2120140,"end":2120500,"confidence":0.9944661,"speaker":"A"},{"text":"abstracted","start":2120500,"end":2121220,"confidence":0.9979248,"speaker":"A"},{"text":"a","start":2121220,"end":2121340,"confidence":0.9995117,"speaker":"A"},{"text":"lot","start":2121340,"end":2121460,"confidence":1,"speaker":"A"},{"text":"of","start":2121460,"end":2121580,"confidence":1,"speaker":"A"},{"text":"this","start":2121580,"end":2121740,"confidence":0.99658203,"speaker":"A"},{"text":"in","start":2121740,"end":2121940,"confidence":0.72802734,"speaker":"A"},{"text":"miskit.","start":2121940,"end":2122620,"confidence":0.83813477,"speaker":"A"},{"text":"So","start":2122940,"end":2123180,"confidence":1,"speaker":"A"},{"text":"that","start":2123180,"end":2123340,"confidence":1,"speaker":"A"},{"text":"way","start":2123340,"end":2123660,"confidence":0.99902344,"speaker":"A"},{"text":"we","start":2123980,"end":2124260,"confidence":1,"speaker":"A"},{"text":"also","start":2124260,"end":2124460,"confidence":0.9995117,"speaker":"A"},{"text":"have","start":2124460,"end":2124740,"confidence":1,"speaker":"A"},{"text":"now","start":2124740,"end":2125100,"confidence":0.99853516,"speaker":"A"},{"text":"a","start":2125580,"end":2125860,"confidence":0.99658203,"speaker":"A"},{"text":"cloud","start":2125860,"end":2126220,"confidence":0.9638672,"speaker":"A"},{"text":"cloud","start":2126540,"end":2127100,"confidence":0.9489746,"speaker":"A"},{"text":"error","start":2127100,"end":2127500,"confidence":0.94311523,"speaker":"A"},{"text":"type","start":2127500,"end":2127980,"confidence":0.99975586,"speaker":"A"},{"text":"which","start":2128540,"end":2128900,"confidence":1,"speaker":"A"},{"text":"gives","start":2128900,"end":2129220,"confidence":1,"speaker":"A"},{"text":"us","start":2129220,"end":2129380,"confidence":1,"speaker":"A"},{"text":"a","start":2129380,"end":2129500,"confidence":1,"speaker":"A"},{"text":"lot","start":2129500,"end":2129660,"confidence":1,"speaker":"A"},{"text":"more","start":2129660,"end":2129980,"confidence":0.9995117,"speaker":"A"},{"text":"info","start":2130060,"end":2130700,"confidence":0.99975586,"speaker":"A"},{"text":"regarding","start":2130860,"end":2131460,"confidence":0.87874347,"speaker":"A"},{"text":"that.","start":2131460,"end":2131820,"confidence":0.99853516,"speaker":"A"},{"text":"So","start":2133900,"end":2134220,"confidence":0.9975586,"speaker":"A"},{"text":"that's","start":2134220,"end":2134540,"confidence":0.9998372,"speaker":"A"},{"text":"how","start":2134540,"end":2134660,"confidence":1,"speaker":"A"},{"text":"we","start":2134660,"end":2134820,"confidence":1,"speaker":"A"},{"text":"handle","start":2134820,"end":2135180,"confidence":0.99975586,"speaker":"A"},{"text":"errors.","start":2135180,"end":2135740,"confidence":0.99912107,"speaker":"A"},{"text":"And","start":2135820,"end":2136140,"confidence":0.99658203,"speaker":"A"},{"text":"everything","start":2136140,"end":2136460,"confidence":0.99902344,"speaker":"A"},{"text":"I","start":2137240,"end":2137360,"confidence":0.9736328,"speaker":"A"},{"text":"do","start":2137360,"end":2137520,"confidence":0.9995117,"speaker":"A"},{"text":"in","start":2137520,"end":2137680,"confidence":0.90283203,"speaker":"A"},{"text":"the","start":2137680,"end":2137800,"confidence":0.92822266,"speaker":"A"},{"text":"abs,","start":2137800,"end":2138080,"confidence":0.4827881,"speaker":"A"},{"text":"the","start":2138080,"end":2138360,"confidence":0.9897461,"speaker":"A"},{"text":"more","start":2138360,"end":2138600,"confidence":0.99072266,"speaker":"A"},{"text":"abstract","start":2138600,"end":2138960,"confidence":0.8538411,"speaker":"A"},{"text":"higher","start":2138960,"end":2139280,"confidence":0.99365234,"speaker":"A"},{"text":"up","start":2139280,"end":2139560,"confidence":0.9970703,"speaker":"A"},{"text":"stuff","start":2139560,"end":2139960,"confidence":0.9713542,"speaker":"A"},{"text":"is","start":2140280,"end":2140680,"confidence":0.99902344,"speaker":"A"},{"text":"done","start":2140680,"end":2141080,"confidence":0.9995117,"speaker":"A"},{"text":"using","start":2141800,"end":2142200,"confidence":1,"speaker":"A"},{"text":"type","start":2142360,"end":2142840,"confidence":0.77783203,"speaker":"A"},{"text":"throws","start":2142840,"end":2143320,"confidence":0.9947917,"speaker":"A"},{"text":"like","start":2143320,"end":2143560,"confidence":0.9794922,"speaker":"A"},{"text":"I","start":2143560,"end":2143760,"confidence":0.99902344,"speaker":"A"},{"text":"have","start":2143760,"end":2143960,"confidence":0.9995117,"speaker":"A"},{"text":"type","start":2143960,"end":2144240,"confidence":0.7751465,"speaker":"A"},{"text":"throws","start":2144240,"end":2144560,"confidence":0.9274089,"speaker":"A"},{"text":"and","start":2144560,"end":2144680,"confidence":0.5439453,"speaker":"A"},{"text":"everything.","start":2144680,"end":2144920,"confidence":0.9995117,"speaker":"A"},{"text":"So","start":2145160,"end":2145560,"confidence":0.9941406,"speaker":"A"},{"text":"that's","start":2145960,"end":2146360,"confidence":0.9996745,"speaker":"A"},{"text":"how","start":2146360,"end":2146440,"confidence":1,"speaker":"A"},{"text":"I","start":2146440,"end":2146560,"confidence":0.9995117,"speaker":"A"},{"text":"handle","start":2146560,"end":2146960,"confidence":0.9951172,"speaker":"A"},{"text":"that.","start":2146960,"end":2147240,"confidence":0.9970703,"speaker":"A"},{"text":"Let","start":2148600,"end":2148880,"confidence":0.97753906,"speaker":"A"},{"text":"me","start":2148880,"end":2149040,"confidence":0.9995117,"speaker":"A"},{"text":"check","start":2149040,"end":2149400,"confidence":0.99780273,"speaker":"A"},{"text":"one","start":2150600,"end":2150920,"confidence":0.99560547,"speaker":"A"},{"text":"last","start":2150920,"end":2151160,"confidence":0.99853516,"speaker":"A"},{"text":"piece","start":2151160,"end":2151440,"confidence":1,"speaker":"A"},{"text":"I","start":2151440,"end":2151560,"confidence":0.99853516,"speaker":"A"},{"text":"wanted","start":2151560,"end":2151800,"confidence":0.99902344,"speaker":"A"},{"text":"to","start":2151800,"end":2151920,"confidence":0.99902344,"speaker":"A"},{"text":"cover.","start":2151920,"end":2152200,"confidence":0.9980469,"speaker":"A"},{"text":"The","start":2154920,"end":2155200,"confidence":0.3737793,"speaker":"A"},{"text":"last","start":2155200,"end":2155360,"confidence":0.9980469,"speaker":"A"},{"text":"piece","start":2155360,"end":2155600,"confidence":0.9995117,"speaker":"A"},{"text":"I","start":2155600,"end":2155720,"confidence":0.97998047,"speaker":"A"},{"text":"want","start":2155720,"end":2155840,"confidence":0.9321289,"speaker":"A"},{"text":"to","start":2155840,"end":2155960,"confidence":0.9916992,"speaker":"A"},{"text":"cover","start":2155960,"end":2156160,"confidence":1,"speaker":"A"},{"text":"is","start":2156160,"end":2156520,"confidence":0.99902344,"speaker":"A"},{"text":"really","start":2156760,"end":2157120,"confidence":0.9995117,"speaker":"A"},{"text":"cool.","start":2157120,"end":2157440,"confidence":0.99975586,"speaker":"A"},{"text":"And","start":2157440,"end":2157680,"confidence":0.7548828,"speaker":"A"},{"text":"that","start":2157680,"end":2157920,"confidence":1,"speaker":"A"},{"text":"is","start":2157920,"end":2158200,"confidence":0.9995117,"speaker":"A"},{"text":"the","start":2158200,"end":2158520,"confidence":1,"speaker":"A"},{"text":"authentication","start":2158520,"end":2159280,"confidence":0.9998779,"speaker":"A"},{"text":"layer.","start":2159280,"end":2159800,"confidence":0.9975586,"speaker":"A"},{"text":"So","start":2160200,"end":2160480,"confidence":0.9770508,"speaker":"A"},{"text":"Open","start":2160480,"end":2160720,"confidence":0.9995117,"speaker":"A"},{"text":"API","start":2160720,"end":2161320,"confidence":0.9436035,"speaker":"A"},{"text":"provides","start":2161320,"end":2161920,"confidence":0.99975586,"speaker":"A"},{"text":"what's","start":2161920,"end":2162240,"confidence":0.99902344,"speaker":"A"},{"text":"called","start":2162240,"end":2162480,"confidence":1,"speaker":"A"},{"text":"middleware","start":2162480,"end":2163160,"confidence":0.9995117,"speaker":"A"},{"text":"and","start":2164440,"end":2164680,"confidence":0.9550781,"speaker":"A"},{"text":"that","start":2164760,"end":2165080,"confidence":0.99902344,"speaker":"A"},{"text":"allows","start":2165080,"end":2165440,"confidence":1,"speaker":"A"},{"text":"you","start":2165440,"end":2165640,"confidence":0.9995117,"speaker":"A"},{"text":"to,","start":2165640,"end":2165960,"confidence":0.99072266,"speaker":"A"},{"text":"when","start":2166200,"end":2166480,"confidence":0.99658203,"speaker":"A"},{"text":"you","start":2166480,"end":2166600,"confidence":0.9892578,"speaker":"A"},{"text":"create","start":2166600,"end":2166720,"confidence":0.9995117,"speaker":"A"},{"text":"a","start":2166720,"end":2166880,"confidence":0.99902344,"speaker":"A"},{"text":"client","start":2166880,"end":2167120,"confidence":0.99975586,"speaker":"A"},{"text":"or","start":2167120,"end":2167320,"confidence":0.9995117,"speaker":"A"},{"text":"a","start":2167320,"end":2167520,"confidence":0.9916992,"speaker":"A"},{"text":"server,","start":2167520,"end":2167840,"confidence":0.99975586,"speaker":"A"},{"text":"you","start":2167840,"end":2167960,"confidence":0.99902344,"speaker":"A"},{"text":"can","start":2167960,"end":2168080,"confidence":1,"speaker":"A"},{"text":"plug","start":2168080,"end":2168360,"confidence":0.99975586,"speaker":"A"},{"text":"that","start":2168360,"end":2168560,"confidence":0.9995117,"speaker":"A"},{"text":"in","start":2168560,"end":2168760,"confidence":0.9975586,"speaker":"A"},{"text":"and","start":2168760,"end":2168960,"confidence":0.9980469,"speaker":"A"},{"text":"it","start":2168960,"end":2169120,"confidence":0.99902344,"speaker":"A"},{"text":"will","start":2169120,"end":2169280,"confidence":0.99902344,"speaker":"A"},{"text":"handle","start":2169280,"end":2169800,"confidence":0.99902344,"speaker":"A"},{"text":"like","start":2169880,"end":2170240,"confidence":0.9291992,"speaker":"A"},{"text":"let's","start":2170240,"end":2170520,"confidence":0.99934894,"speaker":"A"},{"text":"say","start":2170520,"end":2170640,"confidence":1,"speaker":"A"},{"text":"you","start":2170640,"end":2170760,"confidence":1,"speaker":"A"},{"text":"need","start":2170760,"end":2170880,"confidence":1,"speaker":"A"},{"text":"to","start":2170880,"end":2171000,"confidence":1,"speaker":"A"},{"text":"make","start":2171000,"end":2171120,"confidence":1,"speaker":"A"},{"text":"modifications","start":2171120,"end":2171840,"confidence":0.9995117,"speaker":"A"},{"text":"with","start":2171840,"end":2172080,"confidence":0.99902344,"speaker":"A"},{"text":"the","start":2172080,"end":2172240,"confidence":0.9951172,"speaker":"A"},{"text":"request","start":2172240,"end":2172600,"confidence":0.9995117,"speaker":"A"},{"text":"or","start":2172600,"end":2172800,"confidence":0.98779297,"speaker":"A"},{"text":"response.","start":2172800,"end":2173400,"confidence":0.9970703,"speaker":"A"},{"text":"When","start":2173640,"end":2173920,"confidence":1,"speaker":"A"},{"text":"it","start":2173920,"end":2174080,"confidence":0.99902344,"speaker":"A"},{"text":"comes","start":2174080,"end":2174280,"confidence":1,"speaker":"A"},{"text":"in,","start":2174280,"end":2174600,"confidence":0.99658203,"speaker":"A"},{"text":"you","start":2174680,"end":2174960,"confidence":1,"speaker":"A"},{"text":"can","start":2174960,"end":2175120,"confidence":0.9995117,"speaker":"A"},{"text":"intercept","start":2175120,"end":2175520,"confidence":0.8586426,"speaker":"A"},{"text":"it","start":2175520,"end":2175760,"confidence":0.9995117,"speaker":"A"},{"text":"and","start":2175760,"end":2175880,"confidence":0.9995117,"speaker":"A"},{"text":"make","start":2175880,"end":2176040,"confidence":0.9995117,"speaker":"A"},{"text":"whatever","start":2176040,"end":2176360,"confidence":0.9995117,"speaker":"A"},{"text":"modifications","start":2176360,"end":2177040,"confidence":0.99886066,"speaker":"A"},{"text":"you","start":2177040,"end":2177280,"confidence":0.9995117,"speaker":"A"},{"text":"want","start":2177280,"end":2177440,"confidence":0.9277344,"speaker":"A"},{"text":"to","start":2177440,"end":2177560,"confidence":0.9980469,"speaker":"A"},{"text":"make.","start":2177560,"end":2177800,"confidence":0.9980469,"speaker":"A"},{"text":"And","start":2179239,"end":2179519,"confidence":0.9013672,"speaker":"A"},{"text":"in","start":2179519,"end":2179640,"confidence":1,"speaker":"A"},{"text":"this","start":2179640,"end":2179800,"confidence":1,"speaker":"A"},{"text":"case","start":2179800,"end":2180120,"confidence":1,"speaker":"A"},{"text":"what","start":2180840,"end":2181160,"confidence":0.9995117,"speaker":"A"},{"text":"we've","start":2181160,"end":2181440,"confidence":0.9941406,"speaker":"A"},{"text":"done","start":2181440,"end":2181720,"confidence":1,"speaker":"A"},{"text":"is","start":2181720,"end":2182120,"confidence":0.9970703,"speaker":"A"},{"text":"I've","start":2182520,"end":2182880,"confidence":0.9954427,"speaker":"A"},{"text":"created","start":2182880,"end":2183320,"confidence":0.99975586,"speaker":"A"},{"text":"an","start":2184520,"end":2184840,"confidence":0.9926758,"speaker":"A"},{"text":"authentication","start":2184840,"end":2185480,"confidence":1,"speaker":"A"},{"text":"middleware","start":2185480,"end":2186200,"confidence":0.9993164,"speaker":"A"},{"text":"which","start":2187480,"end":2187840,"confidence":0.99902344,"speaker":"A"},{"text":"then","start":2187840,"end":2188200,"confidence":0.99902344,"speaker":"A"},{"text":"sees","start":2188600,"end":2189080,"confidence":0.8354492,"speaker":"A"},{"text":"if","start":2189080,"end":2189280,"confidence":0.99853516,"speaker":"A"},{"text":"you","start":2189280,"end":2189480,"confidence":0.99365234,"speaker":"A"},{"text":"have","start":2189480,"end":2189800,"confidence":0.9946289,"speaker":"A"},{"text":"what's","start":2191430,"end":2191670,"confidence":0.9420573,"speaker":"A"},{"text":"called","start":2191670,"end":2191790,"confidence":1,"speaker":"A"},{"text":"a","start":2191790,"end":2191910,"confidence":0.9916992,"speaker":"A"},{"text":"token","start":2191910,"end":2192270,"confidence":0.9996745,"speaker":"A"},{"text":"manager","start":2192270,"end":2192870,"confidence":0.9995117,"speaker":"A"},{"text":"and","start":2193990,"end":2194390,"confidence":0.98828125,"speaker":"A"},{"text":"an","start":2194390,"end":2194750,"confidence":0.7910156,"speaker":"A"},{"text":"authentic","start":2194750,"end":2195310,"confidence":0.97542316,"speaker":"A"},{"text":"you","start":2195310,"end":2195470,"confidence":0.9970703,"speaker":"A"},{"text":"have","start":2195470,"end":2195630,"confidence":1,"speaker":"A"},{"text":"that","start":2195630,"end":2195870,"confidence":0.9995117,"speaker":"A"},{"text":"and","start":2195870,"end":2196190,"confidence":0.9975586,"speaker":"A"},{"text":"an","start":2196190,"end":2196430,"confidence":0.9980469,"speaker":"A"},{"text":"authentication","start":2196430,"end":2197070,"confidence":0.99938965,"speaker":"A"},{"text":"method.","start":2197070,"end":2197590,"confidence":0.9983724,"speaker":"A"},{"text":"And","start":2198070,"end":2198430,"confidence":0.9921875,"speaker":"A"},{"text":"the","start":2198430,"end":2198670,"confidence":1,"speaker":"A"},{"text":"way","start":2198670,"end":2198790,"confidence":1,"speaker":"A"},{"text":"it","start":2198790,"end":2198910,"confidence":0.99902344,"speaker":"A"},{"text":"works","start":2198910,"end":2199350,"confidence":0.9995117,"speaker":"A"},{"text":"is","start":2199510,"end":2199910,"confidence":0.99902344,"speaker":"A"},{"text":"you","start":2199910,"end":2200230,"confidence":1,"speaker":"A"},{"text":"pick","start":2200230,"end":2200550,"confidence":0.99853516,"speaker":"A"},{"text":"what","start":2201190,"end":2201550,"confidence":0.99365234,"speaker":"A"},{"text":"type","start":2201550,"end":2201830,"confidence":0.99975586,"speaker":"A"},{"text":"of","start":2201830,"end":2201990,"confidence":0.9995117,"speaker":"A"},{"text":"authentication","start":2201990,"end":2202550,"confidence":0.9998779,"speaker":"A"},{"text":"you","start":2202550,"end":2202710,"confidence":0.99902344,"speaker":"A"},{"text":"want","start":2202710,"end":2202830,"confidence":0.9165039,"speaker":"A"},{"text":"to","start":2202830,"end":2202950,"confidence":0.99609375,"speaker":"A"},{"text":"use.","start":2202950,"end":2203070,"confidence":1,"speaker":"A"},{"text":"If","start":2203070,"end":2203230,"confidence":1,"speaker":"A"},{"text":"you","start":2203230,"end":2203350,"confidence":1,"speaker":"A"},{"text":"already","start":2203350,"end":2203510,"confidence":0.99853516,"speaker":"A"},{"text":"have","start":2203510,"end":2203670,"confidence":1,"speaker":"A"},{"text":"like","start":2203670,"end":2203790,"confidence":0.99560547,"speaker":"A"},{"text":"a","start":2203790,"end":2203910,"confidence":0.9995117,"speaker":"A"},{"text":"pre","start":2203910,"end":2204030,"confidence":1,"speaker":"A"},{"text":"existing","start":2204030,"end":2204430,"confidence":0.98551434,"speaker":"A"},{"text":"web","start":2204430,"end":2204670,"confidence":0.99975586,"speaker":"A"},{"text":"token","start":2204670,"end":2205190,"confidence":0.9552409,"speaker":"A"},{"text":"or","start":2205590,"end":2205950,"confidence":0.99853516,"speaker":"A"},{"text":"you","start":2205950,"end":2206190,"confidence":0.99853516,"speaker":"A"},{"text":"already","start":2206190,"end":2206470,"confidence":0.99853516,"speaker":"A"},{"text":"have,","start":2206470,"end":2206789,"confidence":0.92626953,"speaker":"A"},{"text":"or","start":2206789,"end":2207070,"confidence":0.95996094,"speaker":"A"},{"text":"you,","start":2207070,"end":2207350,"confidence":0.9916992,"speaker":"A"},{"text":"you","start":2207350,"end":2207550,"confidence":0.9770508,"speaker":"A"},{"text":"know,","start":2207550,"end":2207710,"confidence":0.9716797,"speaker":"A"},{"text":"have","start":2207710,"end":2207910,"confidence":0.6328125,"speaker":"A"},{"text":"your","start":2207910,"end":2208110,"confidence":0.99853516,"speaker":"A"},{"text":"key","start":2208110,"end":2208310,"confidence":0.99609375,"speaker":"A"},{"text":"ID","start":2208310,"end":2208590,"confidence":0.97753906,"speaker":"A"},{"text":"and","start":2208590,"end":2208830,"confidence":0.99902344,"speaker":"A"},{"text":"your","start":2208830,"end":2208990,"confidence":0.99902344,"speaker":"A"},{"text":"private","start":2208990,"end":2209230,"confidence":1,"speaker":"A"},{"text":"key","start":2209230,"end":2209510,"confidence":0.9995117,"speaker":"A"},{"text":"already,","start":2209510,"end":2209830,"confidence":0.99560547,"speaker":"A"},{"text":"or","start":2209910,"end":2210190,"confidence":0.9995117,"speaker":"A"},{"text":"you","start":2210190,"end":2210350,"confidence":0.9995117,"speaker":"A"},{"text":"just","start":2210350,"end":2210510,"confidence":1,"speaker":"A"},{"text":"have","start":2210510,"end":2210670,"confidence":0.9995117,"speaker":"A"},{"text":"the","start":2210670,"end":2210790,"confidence":0.98339844,"speaker":"A"},{"text":"API","start":2210790,"end":2211190,"confidence":0.9992676,"speaker":"A"},{"text":"token.","start":2211190,"end":2211750,"confidence":0.99934894,"speaker":"A"},{"text":"We've","start":2212390,"end":2212790,"confidence":0.9996745,"speaker":"A"},{"text":"created","start":2212790,"end":2213190,"confidence":0.9995117,"speaker":"A"},{"text":"basically","start":2213190,"end":2213590,"confidence":0.9995117,"speaker":"A"},{"text":"a","start":2213590,"end":2213750,"confidence":0.99609375,"speaker":"A"},{"text":"middleware","start":2213750,"end":2214270,"confidence":0.99716794,"speaker":"A"},{"text":"that","start":2214270,"end":2214470,"confidence":0.99902344,"speaker":"A"},{"text":"uses","start":2214470,"end":2214870,"confidence":0.9992676,"speaker":"A"},{"text":"that.","start":2214870,"end":2215190,"confidence":0.98339844,"speaker":"A"},{"text":"So","start":2216560,"end":2216800,"confidence":0.7055664,"speaker":"A"},{"text":"this","start":2218880,"end":2219120,"confidence":0.9995117,"speaker":"A"},{"text":"is","start":2219120,"end":2219280,"confidence":0.99902344,"speaker":"A"},{"text":"how","start":2219280,"end":2219560,"confidence":1,"speaker":"A"},{"text":"it","start":2219560,"end":2219840,"confidence":0.9995117,"speaker":"A"},{"text":"creates","start":2219840,"end":2220200,"confidence":0.99902344,"speaker":"A"},{"text":"the","start":2220200,"end":2220360,"confidence":0.9995117,"speaker":"A"},{"text":"headers","start":2220360,"end":2220800,"confidence":0.99902344,"speaker":"A"},{"text":"for","start":2221040,"end":2221360,"confidence":0.98583984,"speaker":"A"},{"text":"server","start":2221360,"end":2221720,"confidence":0.9992676,"speaker":"A"},{"text":"to","start":2221720,"end":2221920,"confidence":0.96972656,"speaker":"A"},{"text":"server.","start":2221920,"end":2222400,"confidence":0.9992676,"speaker":"A"},{"text":"So","start":2222800,"end":2223040,"confidence":0.8354492,"speaker":"A"},{"text":"it","start":2223040,"end":2223160,"confidence":0.98583984,"speaker":"A"},{"text":"does","start":2223160,"end":2223320,"confidence":1,"speaker":"A"},{"text":"all","start":2223320,"end":2223480,"confidence":1,"speaker":"A"},{"text":"this","start":2223480,"end":2223640,"confidence":0.9970703,"speaker":"A"},{"text":"for","start":2223640,"end":2223840,"confidence":0.9995117,"speaker":"A"},{"text":"us.","start":2223840,"end":2224160,"confidence":0.99072266,"speaker":"A"},{"text":"And","start":2225760,"end":2226040,"confidence":0.6791992,"speaker":"A"},{"text":"then","start":2226040,"end":2226320,"confidence":0.9941406,"speaker":"A"},{"text":"what","start":2227520,"end":2227760,"confidence":0.9873047,"speaker":"A"},{"text":"I","start":2227760,"end":2227880,"confidence":0.9980469,"speaker":"A"},{"text":"added,","start":2227880,"end":2228160,"confidence":0.99658203,"speaker":"A"},{"text":"which","start":2228480,"end":2228760,"confidence":0.9970703,"speaker":"A"},{"text":"I","start":2228760,"end":2228920,"confidence":0.9995117,"speaker":"A"},{"text":"think","start":2228920,"end":2229040,"confidence":1,"speaker":"A"},{"text":"is","start":2229040,"end":2229160,"confidence":0.9975586,"speaker":"A"},{"text":"really","start":2229160,"end":2229320,"confidence":0.9995117,"speaker":"A"},{"text":"nice,","start":2229320,"end":2229600,"confidence":1,"speaker":"A"},{"text":"is","start":2229600,"end":2229800,"confidence":0.68310547,"speaker":"A"},{"text":"called","start":2229800,"end":2229960,"confidence":0.9995117,"speaker":"A"},{"text":"the","start":2229960,"end":2230120,"confidence":0.9975586,"speaker":"A"},{"text":"adaptive","start":2230120,"end":2230720,"confidence":0.9437256,"speaker":"A"},{"text":"token","start":2230720,"end":2231240,"confidence":0.84195966,"speaker":"A"},{"text":"manager.","start":2231240,"end":2231760,"confidence":0.9963379,"speaker":"A"},{"text":"And","start":2232240,"end":2232520,"confidence":0.6923828,"speaker":"A"},{"text":"the","start":2232520,"end":2232680,"confidence":0.9995117,"speaker":"A"},{"text":"idea","start":2232680,"end":2233000,"confidence":1,"speaker":"A"},{"text":"with","start":2233000,"end":2233160,"confidence":0.99609375,"speaker":"A"},{"text":"that","start":2233160,"end":2233360,"confidence":0.9995117,"speaker":"A"},{"text":"is","start":2233360,"end":2233600,"confidence":0.9975586,"speaker":"A"},{"text":"like","start":2233600,"end":2233880,"confidence":0.8354492,"speaker":"A"},{"text":"let's","start":2233880,"end":2234240,"confidence":0.9013672,"speaker":"A"},{"text":"say","start":2234240,"end":2234560,"confidence":0.9995117,"speaker":"A"},{"text":"you're","start":2236960,"end":2237360,"confidence":0.9977214,"speaker":"A"},{"text":"using","start":2237360,"end":2237520,"confidence":0.9995117,"speaker":"A"},{"text":"a","start":2237520,"end":2237720,"confidence":0.99902344,"speaker":"A"},{"text":"client","start":2237720,"end":2238160,"confidence":0.99975586,"speaker":"A"},{"text":"and","start":2238240,"end":2238560,"confidence":0.9926758,"speaker":"A"},{"text":"you","start":2238560,"end":2238880,"confidence":0.99902344,"speaker":"A"},{"text":"have","start":2238880,"end":2239280,"confidence":1,"speaker":"A"},{"text":"the","start":2239280,"end":2239560,"confidence":0.9995117,"speaker":"A"},{"text":"web","start":2239560,"end":2239800,"confidence":0.9995117,"speaker":"A"},{"text":"authentication","start":2239800,"end":2240480,"confidence":0.8408203,"speaker":"A"},{"text":"token","start":2240480,"end":2240920,"confidence":0.9995117,"speaker":"A"},{"text":"now","start":2240920,"end":2241200,"confidence":0.91308594,"speaker":"A"},{"text":"and","start":2241440,"end":2241720,"confidence":0.94628906,"speaker":"A"},{"text":"then","start":2241720,"end":2242000,"confidence":0.97216797,"speaker":"A"},{"text":"this","start":2242080,"end":2242360,"confidence":0.9975586,"speaker":"A"},{"text":"allows","start":2242360,"end":2242640,"confidence":1,"speaker":"A"},{"text":"you","start":2242640,"end":2242760,"confidence":0.9995117,"speaker":"A"},{"text":"to","start":2242760,"end":2242920,"confidence":0.9980469,"speaker":"A"},{"text":"upgrade","start":2242920,"end":2243440,"confidence":0.9767253,"speaker":"A"},{"text":"with","start":2243810,"end":2243970,"confidence":0.9770508,"speaker":"A"},{"text":"that","start":2243970,"end":2244170,"confidence":0.9995117,"speaker":"A"},{"text":"web","start":2244170,"end":2244410,"confidence":0.998291,"speaker":"A"},{"text":"authentication","start":2244410,"end":2245090,"confidence":0.99938965,"speaker":"A"},{"text":"token","start":2245090,"end":2245450,"confidence":0.9991862,"speaker":"A"},{"text":"to","start":2245450,"end":2245610,"confidence":0.99560547,"speaker":"A"},{"text":"the","start":2245610,"end":2245770,"confidence":1,"speaker":"A"},{"text":"private","start":2245770,"end":2245970,"confidence":1,"speaker":"A"},{"text":"database","start":2245970,"end":2246490,"confidence":0.9998372,"speaker":"A"},{"text":"and","start":2246490,"end":2246690,"confidence":0.99853516,"speaker":"A"},{"text":"have","start":2246690,"end":2246930,"confidence":0.99560547,"speaker":"A"},{"text":"access","start":2246930,"end":2247210,"confidence":1,"speaker":"A"},{"text":"to","start":2247210,"end":2247450,"confidence":0.9995117,"speaker":"A"},{"text":"that.","start":2247450,"end":2247730,"confidence":0.9995117,"speaker":"A"},{"text":"So","start":2250530,"end":2250850,"confidence":0.97558594,"speaker":"A"},{"text":"and","start":2250850,"end":2251050,"confidence":0.97558594,"speaker":"A"},{"text":"then","start":2251050,"end":2251210,"confidence":0.97753906,"speaker":"A"},{"text":"all","start":2251210,"end":2251490,"confidence":0.9658203,"speaker":"A"},{"text":"the,","start":2251490,"end":2251890,"confidence":0.9921875,"speaker":"A"},{"text":"all","start":2252690,"end":2252970,"confidence":0.9013672,"speaker":"A"},{"text":"the","start":2252970,"end":2253170,"confidence":0.99609375,"speaker":"A"},{"text":"signing","start":2253170,"end":2253610,"confidence":0.99658203,"speaker":"A"},{"text":"is","start":2253610,"end":2253770,"confidence":0.9926758,"speaker":"A"},{"text":"done","start":2253770,"end":2253970,"confidence":1,"speaker":"A"},{"text":"before","start":2253970,"end":2254290,"confidence":0.86816406,"speaker":"A"},{"text":"you","start":2254290,"end":2254610,"confidence":0.99902344,"speaker":"A"},{"text":"in","start":2254610,"end":2254810,"confidence":0.9550781,"speaker":"A"},{"text":"miskit","start":2254810,"end":2255490,"confidence":0.8145752,"speaker":"A"},{"text":"for","start":2255650,"end":2256010,"confidence":0.99902344,"speaker":"A"},{"text":"the","start":2256010,"end":2256250,"confidence":0.99902344,"speaker":"A"},{"text":"server","start":2256250,"end":2256530,"confidence":0.99902344,"speaker":"A"},{"text":"to","start":2256530,"end":2256690,"confidence":0.8510742,"speaker":"A"},{"text":"server","start":2256690,"end":2257050,"confidence":0.9995117,"speaker":"A"},{"text":"because","start":2257050,"end":2257250,"confidence":0.9995117,"speaker":"A"},{"text":"stuff","start":2257250,"end":2257490,"confidence":0.9991862,"speaker":"A"},{"text":"that","start":2257490,"end":2257650,"confidence":0.68603516,"speaker":"A"},{"text":"needs","start":2257650,"end":2257850,"confidence":0.9995117,"speaker":"A"},{"text":"to","start":2257850,"end":2257970,"confidence":1,"speaker":"A"},{"text":"be","start":2257970,"end":2258090,"confidence":1,"speaker":"A"},{"text":"signed,","start":2258090,"end":2258330,"confidence":0.79589844,"speaker":"A"},{"text":"etc.","start":2258330,"end":2259010,"confidence":0.88311,"speaker":"A"},{"text":"And","start":2259570,"end":2259849,"confidence":0.99609375,"speaker":"A"},{"text":"it","start":2259849,"end":2260010,"confidence":0.99902344,"speaker":"A"},{"text":"takes","start":2260010,"end":2260250,"confidence":1,"speaker":"A"},{"text":"care","start":2260250,"end":2260410,"confidence":1,"speaker":"A"},{"text":"of","start":2260410,"end":2260610,"confidence":1,"speaker":"A"},{"text":"all","start":2260610,"end":2260850,"confidence":0.9951172,"speaker":"A"},{"text":"that.","start":2260850,"end":2261170,"confidence":0.99560547,"speaker":"A"},{"text":"All","start":2261570,"end":2261890,"confidence":0.9902344,"speaker":"A"},{"text":"stuff","start":2261890,"end":2262170,"confidence":0.9947917,"speaker":"A"},{"text":"that","start":2262170,"end":2262450,"confidence":0.99853516,"speaker":"A"},{"text":"Claude","start":2262690,"end":2263330,"confidence":0.7474365,"speaker":"A"},{"text":"was","start":2263330,"end":2263650,"confidence":0.9995117,"speaker":"A"},{"text":"essentially","start":2263650,"end":2264210,"confidence":0.9995117,"speaker":"A"},{"text":"able","start":2264210,"end":2264450,"confidence":0.9980469,"speaker":"A"},{"text":"to","start":2264450,"end":2264770,"confidence":1,"speaker":"A"},{"text":"decipher","start":2264850,"end":2265610,"confidence":0.99593097,"speaker":"A"},{"text":"from","start":2265610,"end":2265970,"confidence":0.9995117,"speaker":"A"},{"text":"the","start":2266610,"end":2267010,"confidence":0.99072266,"speaker":"A"},{"text":"documentation.","start":2269340,"end":2270060,"confidence":0.9116211,"speaker":"A"},{"text":"There's","start":2272620,"end":2273020,"confidence":0.9972331,"speaker":"A"},{"text":"one","start":2273020,"end":2273140,"confidence":1,"speaker":"A"},{"text":"more","start":2273140,"end":2273300,"confidence":1,"speaker":"A"},{"text":"thing","start":2273300,"end":2273460,"confidence":1,"speaker":"A"},{"text":"I","start":2273460,"end":2273620,"confidence":0.9995117,"speaker":"A"},{"text":"wanted","start":2273620,"end":2273860,"confidence":0.9975586,"speaker":"A"},{"text":"to","start":2273860,"end":2274020,"confidence":1,"speaker":"A"},{"text":"show.","start":2274020,"end":2274300,"confidence":0.99902344,"speaker":"A"},{"text":"If","start":2276380,"end":2276660,"confidence":0.9995117,"speaker":"A"},{"text":"you","start":2276660,"end":2276780,"confidence":1,"speaker":"A"},{"text":"want","start":2276780,"end":2276860,"confidence":0.9921875,"speaker":"A"},{"text":"to","start":2276860,"end":2276980,"confidence":0.9995117,"speaker":"A"},{"text":"hop","start":2276980,"end":2277140,"confidence":0.99853516,"speaker":"A"},{"text":"in","start":2277140,"end":2277300,"confidence":0.9995117,"speaker":"A"},{"text":"with","start":2277300,"end":2277460,"confidence":1,"speaker":"A"},{"text":"a","start":2277460,"end":2277620,"confidence":0.9941406,"speaker":"A"},{"text":"question","start":2277620,"end":2277900,"confidence":1,"speaker":"A"},{"text":"while","start":2278380,"end":2278740,"confidence":0.9946289,"speaker":"A"},{"text":"I","start":2278740,"end":2279100,"confidence":0.99902344,"speaker":"A"},{"text":"pull","start":2279260,"end":2279620,"confidence":0.9995117,"speaker":"A"},{"text":"something","start":2279620,"end":2279860,"confidence":1,"speaker":"A"},{"text":"up,","start":2279860,"end":2280220,"confidence":0.99902344,"speaker":"A"},{"text":"feel","start":2280300,"end":2280620,"confidence":0.9995117,"speaker":"A"},{"text":"free.","start":2280620,"end":2280940,"confidence":1,"speaker":"A"},{"text":"No","start":2301190,"end":2301350,"confidence":0.9892578,"speaker":"A"},{"text":"questions.","start":2301350,"end":2301910,"confidence":0.9995117,"speaker":"A"},{"text":"Cool.","start":2303910,"end":2304390,"confidence":0.8347168,"speaker":"A"},{"text":"So","start":2304790,"end":2305030,"confidence":0.9921875,"speaker":"A"},{"text":"I'm","start":2305030,"end":2305190,"confidence":0.94905597,"speaker":"A"},{"text":"going","start":2305190,"end":2305270,"confidence":0.77441406,"speaker":"A"},{"text":"to","start":2305270,"end":2305350,"confidence":0.9980469,"speaker":"A"},{"text":"show","start":2305350,"end":2305510,"confidence":0.9975586,"speaker":"A"},{"text":"one","start":2305510,"end":2305710,"confidence":0.9995117,"speaker":"A"},{"text":"last","start":2305710,"end":2305950,"confidence":0.9995117,"speaker":"A"},{"text":"thing","start":2305950,"end":2306310,"confidence":0.99902344,"speaker":"A"},{"text":"and","start":2306950,"end":2307230,"confidence":0.9921875,"speaker":"A"},{"text":"that","start":2307230,"end":2307430,"confidence":0.9995117,"speaker":"A"},{"text":"is","start":2307430,"end":2307750,"confidence":0.99609375,"speaker":"A"},{"text":"how","start":2308230,"end":2308630,"confidence":0.9995117,"speaker":"A"},{"text":"do","start":2308710,"end":2308990,"confidence":0.99853516,"speaker":"A"},{"text":"we","start":2308990,"end":2309190,"confidence":1,"speaker":"A"},{"text":"actually","start":2309190,"end":2309470,"confidence":0.9970703,"speaker":"A"},{"text":"deploy","start":2309470,"end":2309990,"confidence":1,"speaker":"A"},{"text":"this?","start":2309990,"end":2310310,"confidence":0.9995117,"speaker":"A"},{"text":"Is","start":2313350,"end":2313630,"confidence":0.9980469,"speaker":"A"},{"text":"this","start":2313630,"end":2313830,"confidence":0.9995117,"speaker":"A"},{"text":"too","start":2313830,"end":2314070,"confidence":0.9975586,"speaker":"A"},{"text":"big,","start":2314070,"end":2314350,"confidence":1,"speaker":"A"},{"text":"too","start":2314350,"end":2314590,"confidence":0.98779297,"speaker":"A"},{"text":"small?","start":2314590,"end":2314870,"confidence":0.99853516,"speaker":"A"},{"text":"Looks","start":2316150,"end":2316510,"confidence":0.8227539,"speaker":"A"},{"text":"okay.","start":2316510,"end":2316950,"confidence":0.9710286,"speaker":"A"},{"text":"That","start":2317590,"end":2317870,"confidence":0.97265625,"speaker":"C"},{"text":"looks","start":2317870,"end":2318150,"confidence":0.99902344,"speaker":"C"},{"text":"good.","start":2318150,"end":2318390,"confidence":0.9921875,"speaker":"C"},{"text":"Yeah,","start":2318710,"end":2319030,"confidence":0.992513,"speaker":"B"},{"text":"it","start":2319030,"end":2319110,"confidence":0.79003906,"speaker":"B"},{"text":"looks","start":2319110,"end":2319270,"confidence":0.99902344,"speaker":"B"},{"text":"good.","start":2319270,"end":2319430,"confidence":0.9951172,"speaker":"B"},{"text":"Okay,","start":2319430,"end":2319750,"confidence":0.9550781,"speaker":"A"},{"text":"cool.","start":2319750,"end":2320070,"confidence":0.99121094,"speaker":"A"},{"text":"So","start":2323850,"end":2324050,"confidence":0.9604492,"speaker":"A"},{"text":"essentially","start":2324050,"end":2324530,"confidence":0.9962158,"speaker":"A"},{"text":"what","start":2324530,"end":2324690,"confidence":0.9995117,"speaker":"A"},{"text":"I've","start":2324690,"end":2324930,"confidence":0.99886066,"speaker":"A"},{"text":"done","start":2324930,"end":2325210,"confidence":0.9995117,"speaker":"A"},{"text":"is","start":2325530,"end":2325930,"confidence":0.99365234,"speaker":"A"},{"text":"I'm","start":2326570,"end":2326930,"confidence":0.95214844,"speaker":"A"},{"text":"using","start":2326930,"end":2327210,"confidence":1,"speaker":"A"},{"text":"GitHub","start":2327370,"end":2327890,"confidence":0.9975586,"speaker":"A"},{"text":"Actions.","start":2327890,"end":2328490,"confidence":0.9992676,"speaker":"A"},{"text":"There's","start":2329290,"end":2329690,"confidence":0.9991862,"speaker":"A"},{"text":"a","start":2329690,"end":2329770,"confidence":0.9995117,"speaker":"A"},{"text":"way","start":2329770,"end":2329930,"confidence":1,"speaker":"A"},{"text":"you","start":2329930,"end":2330130,"confidence":0.99902344,"speaker":"A"},{"text":"can.","start":2330130,"end":2330410,"confidence":0.99902344,"speaker":"A"},{"text":"This","start":2333130,"end":2333410,"confidence":0.99902344,"speaker":"A"},{"text":"is","start":2333410,"end":2333530,"confidence":0.99853516,"speaker":"A"},{"text":"all","start":2333530,"end":2333770,"confidence":0.98876953,"speaker":"A"},{"text":"public","start":2334010,"end":2334370,"confidence":1,"speaker":"A"},{"text":"by","start":2334370,"end":2334570,"confidence":0.99902344,"speaker":"A"},{"text":"the","start":2334570,"end":2334690,"confidence":0.9995117,"speaker":"A"},{"text":"way,","start":2334690,"end":2334970,"confidence":1,"speaker":"A"},{"text":"so","start":2335050,"end":2335450,"confidence":0.9321289,"speaker":"A"},{"text":"I","start":2335850,"end":2336130,"confidence":0.99902344,"speaker":"A"},{"text":"will","start":2336130,"end":2336370,"confidence":0.86621094,"speaker":"A"},{"text":"provide","start":2336370,"end":2336689,"confidence":1,"speaker":"A"},{"text":"URLs","start":2336689,"end":2337330,"confidence":0.94067,"speaker":"A"},{"text":"in","start":2337330,"end":2337490,"confidence":0.98828125,"speaker":"A"},{"text":"the","start":2337490,"end":2337650,"confidence":0.9897461,"speaker":"A"},{"text":"Slack","start":2337650,"end":2337970,"confidence":0.998291,"speaker":"A"},{"text":"or","start":2337970,"end":2338170,"confidence":0.9970703,"speaker":"A"},{"text":"something.","start":2338170,"end":2338490,"confidence":0.9995117,"speaker":"A"},{"text":"Let's","start":2339450,"end":2339890,"confidence":0.99853516,"speaker":"A"},{"text":"do","start":2339890,"end":2340050,"confidence":0.9790039,"speaker":"A"},{"text":"this","start":2340050,"end":2340250,"confidence":0.9975586,"speaker":"A"},{"text":"one.","start":2340250,"end":2340570,"confidence":0.99316406,"speaker":"A"},{"text":"So","start":2342410,"end":2342810,"confidence":0.8173828,"speaker":"A"},{"text":"this","start":2343930,"end":2344210,"confidence":0.9995117,"speaker":"A"},{"text":"is","start":2344210,"end":2344370,"confidence":0.9995117,"speaker":"A"},{"text":"a","start":2344370,"end":2344530,"confidence":0.9765625,"speaker":"A"},{"text":"Swift","start":2344530,"end":2344810,"confidence":0.9226074,"speaker":"A"},{"text":"package","start":2344810,"end":2345370,"confidence":0.99768066,"speaker":"A"},{"text":"for","start":2347060,"end":2347220,"confidence":0.97998047,"speaker":"A"},{"text":"Bushel.","start":2347220,"end":2347860,"confidence":0.9685872,"speaker":"A"},{"text":"It's","start":2347860,"end":2348180,"confidence":0.9995117,"speaker":"A"},{"text":"called","start":2348180,"end":2348340,"confidence":0.99853516,"speaker":"A"},{"text":"Bushel","start":2348340,"end":2348780,"confidence":0.90283203,"speaker":"A"},{"text":"Cloud.","start":2348780,"end":2349180,"confidence":0.99658203,"speaker":"A"},{"text":"It","start":2349180,"end":2349420,"confidence":0.9995117,"speaker":"A"},{"text":"pulls","start":2349420,"end":2349700,"confidence":1,"speaker":"A"},{"text":"the","start":2349700,"end":2349820,"confidence":0.98828125,"speaker":"A"},{"text":"stuff","start":2349820,"end":2350060,"confidence":1,"speaker":"A"},{"text":"up","start":2350060,"end":2350300,"confidence":0.9995117,"speaker":"A"},{"text":"from.","start":2350300,"end":2350660,"confidence":0.9970703,"speaker":"A"},{"text":"Uses","start":2351220,"end":2351740,"confidence":0.84887695,"speaker":"A"},{"text":"Miskit","start":2351740,"end":2352340,"confidence":0.9329834,"speaker":"A"},{"text":"to","start":2353540,"end":2353820,"confidence":0.9941406,"speaker":"A"},{"text":"go","start":2353820,"end":2353980,"confidence":1,"speaker":"A"},{"text":"ahead","start":2353980,"end":2354260,"confidence":0.9995117,"speaker":"A"},{"text":"and","start":2354340,"end":2354740,"confidence":0.88720703,"speaker":"A"},{"text":"pull,","start":2356740,"end":2357220,"confidence":0.9621582,"speaker":"A"},{"text":"get","start":2357860,"end":2358140,"confidence":0.99902344,"speaker":"A"},{"text":"access","start":2358140,"end":2358380,"confidence":1,"speaker":"A"},{"text":"to","start":2358380,"end":2358700,"confidence":1,"speaker":"A"},{"text":"CloudKit","start":2358700,"end":2359460,"confidence":0.9325,"speaker":"A"},{"text":"and","start":2359940,"end":2360340,"confidence":0.98291016,"speaker":"A"},{"text":"let","start":2361060,"end":2361340,"confidence":0.99316406,"speaker":"A"},{"text":"me","start":2361340,"end":2361460,"confidence":1,"speaker":"A"},{"text":"go","start":2361460,"end":2361620,"confidence":0.9995117,"speaker":"A"},{"text":"back","start":2361620,"end":2361940,"confidence":1,"speaker":"A"},{"text":"to","start":2361940,"end":2362339,"confidence":0.9995117,"speaker":"A"},{"text":"the","start":2362339,"end":2362620,"confidence":1,"speaker":"A"},{"text":"workflow.","start":2362620,"end":2363300,"confidence":0.96276855,"speaker":"A"},{"text":"How","start":2364100,"end":2364420,"confidence":0.99853516,"speaker":"A"},{"text":"familiar","start":2364420,"end":2364860,"confidence":1,"speaker":"A"},{"text":"are","start":2364860,"end":2365020,"confidence":0.9995117,"speaker":"A"},{"text":"you","start":2365020,"end":2365180,"confidence":0.9995117,"speaker":"A"},{"text":"with","start":2365180,"end":2365380,"confidence":1,"speaker":"A"},{"text":"GitHub","start":2365380,"end":2365860,"confidence":0.87939453,"speaker":"A"},{"text":"workflows?","start":2365860,"end":2366580,"confidence":0.9026367,"speaker":"A"},{"text":"Sadly","start":2369860,"end":2370300,"confidence":0.99576825,"speaker":"C"},{"text":"not","start":2370300,"end":2370500,"confidence":0.9951172,"speaker":"C"},{"text":"had","start":2370500,"end":2370660,"confidence":0.9980469,"speaker":"C"},{"text":"the","start":2370660,"end":2370780,"confidence":0.99658203,"speaker":"C"},{"text":"chance","start":2370780,"end":2371020,"confidence":0.99975586,"speaker":"C"},{"text":"to","start":2371020,"end":2371180,"confidence":0.9995117,"speaker":"C"},{"text":"work","start":2371180,"end":2371460,"confidence":1,"speaker":"C"},{"text":"too","start":2371780,"end":2372060,"confidence":0.99560547,"speaker":"C"},{"text":"deeply","start":2372060,"end":2372380,"confidence":0.9991862,"speaker":"C"},{"text":"with","start":2372380,"end":2372500,"confidence":0.9995117,"speaker":"C"},{"text":"them","start":2372500,"end":2372660,"confidence":0.97021484,"speaker":"C"},{"text":"yet.","start":2372660,"end":2372980,"confidence":0.98291016,"speaker":"C"},{"text":"Okay.","start":2373690,"end":2374090,"confidence":0.9503581,"speaker":"A"},{"text":"Basically","start":2375130,"end":2375610,"confidence":0.9987793,"speaker":"A"},{"text":"it's","start":2375610,"end":2375850,"confidence":0.99934894,"speaker":"A"},{"text":"like","start":2375850,"end":2375970,"confidence":0.99072266,"speaker":"A"},{"text":"for","start":2375970,"end":2376170,"confidence":0.9448242,"speaker":"A"},{"text":"CI,","start":2376170,"end":2376610,"confidence":0.97021484,"speaker":"A"},{"text":"but","start":2376610,"end":2376810,"confidence":0.99902344,"speaker":"A"},{"text":"you","start":2376810,"end":2376930,"confidence":0.9995117,"speaker":"A"},{"text":"can","start":2376930,"end":2377050,"confidence":0.9995117,"speaker":"A"},{"text":"also","start":2377050,"end":2377250,"confidence":0.9995117,"speaker":"A"},{"text":"set","start":2377250,"end":2377490,"confidence":1,"speaker":"A"},{"text":"it","start":2377490,"end":2377610,"confidence":0.9995117,"speaker":"A"},{"text":"up","start":2377610,"end":2377730,"confidence":0.9995117,"speaker":"A"},{"text":"on","start":2377730,"end":2377890,"confidence":0.9995117,"speaker":"A"},{"text":"a","start":2377890,"end":2378050,"confidence":0.9980469,"speaker":"A"},{"text":"schedule.","start":2378050,"end":2378570,"confidence":0.8905029,"speaker":"A"},{"text":"So","start":2378890,"end":2379170,"confidence":0.9941406,"speaker":"A"},{"text":"I","start":2379170,"end":2379330,"confidence":1,"speaker":"A"},{"text":"did","start":2379330,"end":2379530,"confidence":0.99902344,"speaker":"A"},{"text":"that","start":2379530,"end":2379850,"confidence":0.99902344,"speaker":"A"},{"text":"and","start":2381290,"end":2381570,"confidence":0.9902344,"speaker":"A"},{"text":"then","start":2381570,"end":2381850,"confidence":0.9980469,"speaker":"A"},{"text":"it","start":2382890,"end":2383170,"confidence":0.99853516,"speaker":"A"},{"text":"runs","start":2383170,"end":2383490,"confidence":0.99975586,"speaker":"A"},{"text":"the","start":2383490,"end":2383610,"confidence":0.6640625,"speaker":"A"},{"text":"scheduled","start":2383610,"end":2384090,"confidence":0.89404297,"speaker":"A"},{"text":"job","start":2384090,"end":2384410,"confidence":1,"speaker":"A"},{"text":"and","start":2384810,"end":2385090,"confidence":0.9975586,"speaker":"A"},{"text":"then","start":2385090,"end":2385250,"confidence":0.99902344,"speaker":"A"},{"text":"I","start":2385250,"end":2385450,"confidence":0.9995117,"speaker":"A"},{"text":"just","start":2385450,"end":2385730,"confidence":0.9995117,"speaker":"A"},{"text":"execute.","start":2385730,"end":2386490,"confidence":0.97875977,"speaker":"A"},{"text":"So","start":2390650,"end":2390930,"confidence":0.9941406,"speaker":"A"},{"text":"then","start":2390930,"end":2391170,"confidence":0.9975586,"speaker":"A"},{"text":"this","start":2391170,"end":2391410,"confidence":1,"speaker":"A"},{"text":"was","start":2391410,"end":2391610,"confidence":0.9995117,"speaker":"A"},{"text":"refactored","start":2391610,"end":2392490,"confidence":0.99283856,"speaker":"A"},{"text":"over","start":2393290,"end":2393690,"confidence":0.99560547,"speaker":"A"},{"text":"here","start":2393690,"end":2394090,"confidence":0.9995117,"speaker":"A"},{"text":"into","start":2394330,"end":2394650,"confidence":0.9741211,"speaker":"A"},{"text":"an","start":2394650,"end":2394890,"confidence":0.99902344,"speaker":"A"},{"text":"action.","start":2394890,"end":2395210,"confidence":0.9995117,"speaker":"A"},{"text":"There","start":2397770,"end":2398090,"confidence":0.89990234,"speaker":"A"},{"text":"we","start":2398090,"end":2398250,"confidence":0.99853516,"speaker":"A"},{"text":"go.","start":2398250,"end":2398490,"confidence":0.99853516,"speaker":"A"},{"text":"And","start":2399540,"end":2399780,"confidence":0.9848633,"speaker":"A"},{"text":"I","start":2401140,"end":2401420,"confidence":0.99658203,"speaker":"A"},{"text":"have","start":2401420,"end":2401580,"confidence":0.9995117,"speaker":"A"},{"text":"all","start":2401580,"end":2401740,"confidence":0.9995117,"speaker":"A"},{"text":"sorts","start":2401740,"end":2402020,"confidence":0.890625,"speaker":"A"},{"text":"of","start":2402020,"end":2402180,"confidence":1,"speaker":"A"},{"text":"stuff","start":2402180,"end":2402380,"confidence":1,"speaker":"A"},{"text":"here","start":2402380,"end":2402660,"confidence":0.9995117,"speaker":"A"},{"text":"for","start":2403060,"end":2403460,"confidence":0.9863281,"speaker":"A"},{"text":"like","start":2405380,"end":2405780,"confidence":0.97021484,"speaker":"A"},{"text":"this","start":2406660,"end":2406940,"confidence":0.9975586,"speaker":"A"},{"text":"is","start":2406940,"end":2407100,"confidence":0.99902344,"speaker":"A"},{"text":"generic","start":2407100,"end":2407700,"confidence":1,"speaker":"A"},{"text":"essentially,","start":2407700,"end":2408420,"confidence":0.9996338,"speaker":"A"},{"text":"but","start":2408500,"end":2408900,"confidence":0.9941406,"speaker":"A"},{"text":"all","start":2410020,"end":2410300,"confidence":0.98828125,"speaker":"A"},{"text":"these,","start":2410300,"end":2410580,"confidence":0.9868164,"speaker":"A"},{"text":"the","start":2410820,"end":2411140,"confidence":0.9223633,"speaker":"A"},{"text":"environment,","start":2411140,"end":2411460,"confidence":1,"speaker":"A"},{"text":"etc.","start":2411700,"end":2412500,"confidence":0.975,"speaker":"A"},{"text":"These","start":2413140,"end":2413420,"confidence":0.9995117,"speaker":"A"},{"text":"are","start":2413420,"end":2413540,"confidence":0.9995117,"speaker":"A"},{"text":"all","start":2413540,"end":2413700,"confidence":0.99853516,"speaker":"A"},{"text":"passed","start":2413700,"end":2414060,"confidence":0.93310547,"speaker":"A"},{"text":"from","start":2414060,"end":2414220,"confidence":1,"speaker":"A"},{"text":"that","start":2414220,"end":2414420,"confidence":0.99902344,"speaker":"A"},{"text":"workflow","start":2414420,"end":2414980,"confidence":0.9741211,"speaker":"A"},{"text":"into","start":2414980,"end":2415260,"confidence":0.99609375,"speaker":"A"},{"text":"here.","start":2415260,"end":2415620,"confidence":0.99902344,"speaker":"A"},{"text":"These","start":2415940,"end":2416220,"confidence":0.9975586,"speaker":"A"},{"text":"are","start":2416220,"end":2416380,"confidence":0.9995117,"speaker":"A"},{"text":"basically","start":2416380,"end":2416820,"confidence":0.9992676,"speaker":"A"},{"text":"either","start":2416820,"end":2417180,"confidence":0.9995117,"speaker":"A"},{"text":"API","start":2417180,"end":2417620,"confidence":0.85180664,"speaker":"A"},{"text":"keys","start":2417620,"end":2417980,"confidence":0.99975586,"speaker":"A"},{"text":"or","start":2417980,"end":2418180,"confidence":0.9995117,"speaker":"A"},{"text":"the","start":2418180,"end":2418420,"confidence":0.99902344,"speaker":"A"},{"text":"information","start":2418420,"end":2418740,"confidence":0.9995117,"speaker":"A"},{"text":"that","start":2418820,"end":2419100,"confidence":0.99902344,"speaker":"A"},{"text":"I","start":2419100,"end":2419260,"confidence":1,"speaker":"A"},{"text":"need","start":2419260,"end":2419540,"confidence":0.9995117,"speaker":"A"},{"text":"for","start":2419620,"end":2420020,"confidence":0.9995117,"speaker":"A"},{"text":"accessing","start":2420500,"end":2421100,"confidence":0.9953613,"speaker":"A"},{"text":"Cloud,","start":2421100,"end":2421460,"confidence":0.9243164,"speaker":"A"},{"text":"the","start":2421460,"end":2421780,"confidence":0.8491211,"speaker":"A"},{"text":"public,","start":2421780,"end":2422100,"confidence":0.765625,"speaker":"A"},{"text":"public","start":2424020,"end":2424380,"confidence":0.9995117,"speaker":"A"},{"text":"database.","start":2424380,"end":2425060,"confidence":0.99869794,"speaker":"A"},{"text":"Right.","start":2425840,"end":2426080,"confidence":0.9008789,"speaker":"A"},{"text":"And","start":2426480,"end":2426760,"confidence":0.9794922,"speaker":"A"},{"text":"then","start":2426760,"end":2427040,"confidence":0.9980469,"speaker":"A"},{"text":"I","start":2427840,"end":2428120,"confidence":0.96435547,"speaker":"A"},{"text":"already","start":2428120,"end":2428360,"confidence":0.99902344,"speaker":"A"},{"text":"pre","start":2428360,"end":2428680,"confidence":0.99853516,"speaker":"A"},{"text":"built","start":2428680,"end":2429200,"confidence":0.8404948,"speaker":"A"},{"text":"the","start":2429760,"end":2430160,"confidence":0.9970703,"speaker":"A"},{"text":"binary.","start":2430160,"end":2430880,"confidence":0.9977214,"speaker":"A"},{"text":"So","start":2431120,"end":2431520,"confidence":0.99316406,"speaker":"A"},{"text":"we","start":2431600,"end":2431880,"confidence":0.9995117,"speaker":"A"},{"text":"already","start":2431880,"end":2432040,"confidence":0.9995117,"speaker":"A"},{"text":"have","start":2432040,"end":2432200,"confidence":0.99902344,"speaker":"A"},{"text":"that.","start":2432200,"end":2432360,"confidence":1,"speaker":"A"},{"text":"We're","start":2432360,"end":2432600,"confidence":0.9973958,"speaker":"A"},{"text":"running","start":2432600,"end":2432840,"confidence":1,"speaker":"A"},{"text":"this","start":2432840,"end":2433120,"confidence":0.99902344,"speaker":"A"},{"text":"on","start":2433200,"end":2433600,"confidence":0.9975586,"speaker":"A"},{"text":"Ubuntu","start":2434880,"end":2435720,"confidence":0.93408203,"speaker":"A"},{"text":"because","start":2435720,"end":2435960,"confidence":0.94970703,"speaker":"A"},{"text":"it's","start":2435960,"end":2436160,"confidence":0.99902344,"speaker":"A"},{"text":"the","start":2436160,"end":2436280,"confidence":0.8647461,"speaker":"A"},{"text":"default.","start":2436280,"end":2436800,"confidence":0.9998779,"speaker":"A"},{"text":"Look","start":2437200,"end":2437480,"confidence":0.9970703,"speaker":"A"},{"text":"at","start":2437480,"end":2437640,"confidence":0.9995117,"speaker":"A"},{"text":"it.","start":2437640,"end":2437920,"confidence":0.9995117,"speaker":"A"},{"text":"If","start":2439200,"end":2439600,"confidence":0.9980469,"speaker":"A"},{"text":"there","start":2439920,"end":2440280,"confidence":1,"speaker":"A"},{"text":"is","start":2440280,"end":2440560,"confidence":0.9995117,"speaker":"A"},{"text":"no","start":2440560,"end":2440880,"confidence":0.9970703,"speaker":"A"},{"text":"binary,","start":2440960,"end":2441639,"confidence":0.9977214,"speaker":"A"},{"text":"it","start":2441639,"end":2441840,"confidence":0.9736328,"speaker":"A"},{"text":"goes","start":2441840,"end":2442000,"confidence":1,"speaker":"A"},{"text":"ahead","start":2442000,"end":2442120,"confidence":0.99853516,"speaker":"A"},{"text":"and","start":2442120,"end":2442320,"confidence":1,"speaker":"A"},{"text":"builds","start":2442320,"end":2442680,"confidence":0.99853516,"speaker":"A"},{"text":"the","start":2442680,"end":2442800,"confidence":1,"speaker":"A"},{"text":"binary","start":2442800,"end":2443280,"confidence":0.9991862,"speaker":"A"},{"text":"for","start":2443280,"end":2443520,"confidence":0.99853516,"speaker":"A"},{"text":"me.","start":2443520,"end":2443840,"confidence":0.9995117,"speaker":"A"},{"text":"So","start":2444000,"end":2444240,"confidence":0.95166016,"speaker":"A"},{"text":"that's","start":2444240,"end":2444400,"confidence":0.9991862,"speaker":"A"},{"text":"what","start":2444400,"end":2444520,"confidence":0.9995117,"speaker":"A"},{"text":"this","start":2444520,"end":2444680,"confidence":1,"speaker":"A"},{"text":"is","start":2444680,"end":2444880,"confidence":1,"speaker":"A"},{"text":"doing.","start":2444880,"end":2445200,"confidence":0.99902344,"speaker":"A"},{"text":"And","start":2447120,"end":2447440,"confidence":0.88671875,"speaker":"A"},{"text":"then","start":2447440,"end":2447760,"confidence":0.99902344,"speaker":"A"},{"text":"we","start":2448800,"end":2449080,"confidence":0.9995117,"speaker":"A"},{"text":"make","start":2449080,"end":2449280,"confidence":0.7973633,"speaker":"A"},{"text":"sure","start":2449280,"end":2449480,"confidence":1,"speaker":"A"},{"text":"the","start":2449480,"end":2449640,"confidence":0.9941406,"speaker":"A"},{"text":"binary","start":2449640,"end":2450080,"confidence":0.92838544,"speaker":"A"},{"text":"works.","start":2450080,"end":2450640,"confidence":0.9995117,"speaker":"A"},{"text":"We","start":2450880,"end":2451120,"confidence":0.41552734,"speaker":"A"},{"text":"make,","start":2451120,"end":2451180,"confidence":0.6088867,"speaker":"A"},{"text":"we","start":2451250,"end":2451330,"confidence":0.6176758,"speaker":"A"},{"text":"make","start":2451330,"end":2451450,"confidence":0.99902344,"speaker":"A"},{"text":"it","start":2451450,"end":2451610,"confidence":0.9550781,"speaker":"A"},{"text":"executable,","start":2451610,"end":2452210,"confidence":0.9968262,"speaker":"A"},{"text":"we","start":2452290,"end":2452650,"confidence":0.99658203,"speaker":"A"},{"text":"validate,","start":2452650,"end":2453290,"confidence":0.9996745,"speaker":"A"},{"text":"make","start":2453290,"end":2453530,"confidence":0.9951172,"speaker":"A"},{"text":"sure","start":2453530,"end":2453730,"confidence":1,"speaker":"A"},{"text":"all","start":2453730,"end":2454050,"confidence":0.99902344,"speaker":"A"},{"text":"the","start":2454050,"end":2454450,"confidence":0.99902344,"speaker":"A"},{"text":"API","start":2455010,"end":2455570,"confidence":0.9987793,"speaker":"A"},{"text":"secrets","start":2455570,"end":2456050,"confidence":0.98339844,"speaker":"A"},{"text":"are","start":2456050,"end":2456250,"confidence":0.99902344,"speaker":"A"},{"text":"there.","start":2456250,"end":2456530,"confidence":0.99902344,"speaker":"A"},{"text":"We","start":2457650,"end":2457970,"confidence":0.9951172,"speaker":"A"},{"text":"then","start":2457970,"end":2458210,"confidence":0.99658203,"speaker":"A"},{"text":"go","start":2458210,"end":2458410,"confidence":0.99853516,"speaker":"A"},{"text":"ahead","start":2458410,"end":2458690,"confidence":0.99853516,"speaker":"A"},{"text":"and","start":2458930,"end":2459290,"confidence":0.9921875,"speaker":"A"},{"text":"this","start":2459290,"end":2459530,"confidence":0.9863281,"speaker":"A"},{"text":"validates","start":2459530,"end":2460010,"confidence":0.99690753,"speaker":"A"},{"text":"the","start":2460010,"end":2460170,"confidence":0.99902344,"speaker":"A"},{"text":"pim.","start":2460170,"end":2460530,"confidence":0.8864746,"speaker":"A"},{"text":"But","start":2460690,"end":2460970,"confidence":0.99853516,"speaker":"A"},{"text":"essentially","start":2460970,"end":2461370,"confidence":0.9954834,"speaker":"A"},{"text":"this","start":2461370,"end":2461530,"confidence":0.9902344,"speaker":"A"},{"text":"is","start":2461530,"end":2461650,"confidence":0.9814453,"speaker":"A"},{"text":"the","start":2461650,"end":2461770,"confidence":0.8173828,"speaker":"A"},{"text":"fun","start":2461770,"end":2462010,"confidence":0.9980469,"speaker":"A"},{"text":"part.","start":2462010,"end":2462370,"confidence":0.9995117,"speaker":"A"},{"text":"We","start":2463410,"end":2463690,"confidence":0.9995117,"speaker":"A"},{"text":"go","start":2463690,"end":2463810,"confidence":0.9995117,"speaker":"A"},{"text":"ahead,","start":2463810,"end":2464050,"confidence":0.99902344,"speaker":"A"},{"text":"we","start":2464050,"end":2464330,"confidence":0.9980469,"speaker":"A"},{"text":"have","start":2464330,"end":2464610,"confidence":0.99902344,"speaker":"A"},{"text":"all","start":2464930,"end":2465290,"confidence":0.99853516,"speaker":"A"},{"text":"our","start":2465290,"end":2465530,"confidence":0.99365234,"speaker":"A"},{"text":"inputs","start":2465530,"end":2466010,"confidence":0.88171387,"speaker":"A"},{"text":"for","start":2466010,"end":2466170,"confidence":0.99902344,"speaker":"A"},{"text":"the","start":2466170,"end":2466290,"confidence":1,"speaker":"A"},{"text":"private","start":2466290,"end":2466490,"confidence":0.99902344,"speaker":"A"},{"text":"key,","start":2466490,"end":2466770,"confidence":0.9995117,"speaker":"A"},{"text":"the","start":2466770,"end":2467089,"confidence":0.9277344,"speaker":"A"},{"text":"key","start":2467089,"end":2467410,"confidence":0.98779297,"speaker":"A"},{"text":"id,","start":2467410,"end":2467730,"confidence":0.97021484,"speaker":"A"},{"text":"environment,","start":2467810,"end":2468210,"confidence":0.99902344,"speaker":"A"},{"text":"container","start":2468690,"end":2469290,"confidence":0.99902344,"speaker":"A"},{"text":"id.","start":2469290,"end":2469570,"confidence":0.99609375,"speaker":"A"},{"text":"And","start":2470610,"end":2470890,"confidence":0.9707031,"speaker":"A"},{"text":"then","start":2470890,"end":2471050,"confidence":0.99902344,"speaker":"A"},{"text":"I","start":2471050,"end":2471170,"confidence":0.99902344,"speaker":"A"},{"text":"use","start":2471170,"end":2471370,"confidence":0.99658203,"speaker":"A"},{"text":"Virtual","start":2471370,"end":2471770,"confidence":0.9996338,"speaker":"A"},{"text":"Buddy","start":2471770,"end":2472090,"confidence":0.98583984,"speaker":"A"},{"text":"for","start":2472090,"end":2472250,"confidence":0.99902344,"speaker":"A"},{"text":"signing","start":2472250,"end":2472650,"confidence":0.9938965,"speaker":"A"},{"text":"verification.","start":2472650,"end":2473410,"confidence":0.99990237,"speaker":"A"},{"text":"And.","start":2474050,"end":2474450,"confidence":0.93603516,"speaker":"A"},{"text":"It","start":2478460,"end":2478580,"confidence":0.9707031,"speaker":"A"},{"text":"then","start":2478580,"end":2478740,"confidence":0.9980469,"speaker":"A"},{"text":"goes","start":2478740,"end":2479060,"confidence":0.99975586,"speaker":"A"},{"text":"in","start":2479060,"end":2479220,"confidence":0.99902344,"speaker":"A"},{"text":"and","start":2479220,"end":2479500,"confidence":0.8173828,"speaker":"A"},{"text":"it","start":2479900,"end":2480300,"confidence":0.99560547,"speaker":"A"},{"text":"runs","start":2481260,"end":2481740,"confidence":1,"speaker":"A"},{"text":"the","start":2481740,"end":2481940,"confidence":0.9995117,"speaker":"A"},{"text":"sync","start":2481940,"end":2482380,"confidence":0.9733073,"speaker":"A"},{"text":"and","start":2483500,"end":2483780,"confidence":0.96435547,"speaker":"A"},{"text":"then","start":2483780,"end":2484060,"confidence":0.97753906,"speaker":"A"},{"text":"we'll","start":2484860,"end":2485220,"confidence":0.8601888,"speaker":"A"},{"text":"go","start":2485220,"end":2485380,"confidence":0.99902344,"speaker":"A"},{"text":"in.","start":2485380,"end":2485660,"confidence":0.9980469,"speaker":"A"},{"text":"Basically","start":2485980,"end":2486460,"confidence":0.9995117,"speaker":"A"},{"text":"it","start":2486460,"end":2486620,"confidence":0.95996094,"speaker":"A"},{"text":"pulls","start":2486620,"end":2486900,"confidence":0.99902344,"speaker":"A"},{"text":"from","start":2486900,"end":2487060,"confidence":1,"speaker":"A"},{"text":"several","start":2487060,"end":2487340,"confidence":0.9995117,"speaker":"A"},{"text":"websites","start":2487340,"end":2488140,"confidence":0.99658203,"speaker":"A"},{"text":"information","start":2489100,"end":2489500,"confidence":1,"speaker":"A"},{"text":"about","start":2489580,"end":2489900,"confidence":0.9995117,"speaker":"A"},{"text":"macrosos,","start":2489900,"end":2490500,"confidence":0.85645,"speaker":"A"},{"text":"restore","start":2490500,"end":2490940,"confidence":0.85498047,"speaker":"A"},{"text":"images","start":2490940,"end":2491380,"confidence":0.998291,"speaker":"A"},{"text":"and","start":2491380,"end":2491620,"confidence":0.9980469,"speaker":"A"},{"text":"checks","start":2491620,"end":2491940,"confidence":0.9996745,"speaker":"A"},{"text":"whether","start":2491940,"end":2492100,"confidence":0.99902344,"speaker":"A"},{"text":"they're","start":2492100,"end":2492380,"confidence":0.98030597,"speaker":"A"},{"text":"signed.","start":2492380,"end":2492939,"confidence":0.80981445,"speaker":"A"},{"text":"And","start":2493340,"end":2493620,"confidence":0.94970703,"speaker":"A"},{"text":"then","start":2493620,"end":2493780,"confidence":0.9970703,"speaker":"A"},{"text":"it","start":2493780,"end":2493940,"confidence":1,"speaker":"A"},{"text":"goes","start":2493940,"end":2494140,"confidence":1,"speaker":"A"},{"text":"ahead","start":2494140,"end":2494340,"confidence":0.99902344,"speaker":"A"},{"text":"and","start":2494340,"end":2494700,"confidence":0.53125,"speaker":"A"},{"text":"it","start":2494780,"end":2495180,"confidence":0.86621094,"speaker":"A"},{"text":"adds","start":2496380,"end":2496900,"confidence":0.99853516,"speaker":"A"},{"text":"those","start":2496900,"end":2497180,"confidence":0.9995117,"speaker":"A"},{"text":"to","start":2497260,"end":2497540,"confidence":1,"speaker":"A"},{"text":"the","start":2497540,"end":2497660,"confidence":1,"speaker":"A"},{"text":"database.","start":2497660,"end":2498260,"confidence":0.9998372,"speaker":"A"},{"text":"And","start":2498260,"end":2498500,"confidence":0.9238281,"speaker":"A"},{"text":"then","start":2498500,"end":2498700,"confidence":0.9902344,"speaker":"A"},{"text":"what","start":2498700,"end":2498900,"confidence":0.9995117,"speaker":"A"},{"text":"this","start":2498900,"end":2499060,"confidence":1,"speaker":"A"},{"text":"does","start":2499060,"end":2499260,"confidence":0.9995117,"speaker":"A"},{"text":"is","start":2499260,"end":2499460,"confidence":0.99902344,"speaker":"A"},{"text":"it","start":2499460,"end":2499620,"confidence":0.86279297,"speaker":"A"},{"text":"exports","start":2499620,"end":2500140,"confidence":0.99902344,"speaker":"A"},{"text":"the","start":2500620,"end":2500940,"confidence":0.99560547,"speaker":"A"},{"text":"information","start":2500940,"end":2501260,"confidence":1,"speaker":"A"},{"text":"in","start":2501500,"end":2501780,"confidence":0.9946289,"speaker":"A"},{"text":"a","start":2501780,"end":2501900,"confidence":0.98046875,"speaker":"A"},{"text":"run.","start":2501900,"end":2502100,"confidence":0.9926758,"speaker":"A"},{"text":"Let's,","start":2502100,"end":2502460,"confidence":0.7273763,"speaker":"A"},{"text":"let's","start":2502460,"end":2502700,"confidence":0.8728841,"speaker":"A"},{"text":"take","start":2502700,"end":2502820,"confidence":0.9921875,"speaker":"A"},{"text":"a","start":2502820,"end":2502940,"confidence":1,"speaker":"A"},{"text":"look,","start":2502940,"end":2503140,"confidence":0.9995117,"speaker":"A"},{"text":"see","start":2503140,"end":2503380,"confidence":0.99902344,"speaker":"A"},{"text":"if","start":2503380,"end":2503500,"confidence":1,"speaker":"A"},{"text":"I","start":2503500,"end":2503580,"confidence":0.9995117,"speaker":"A"},{"text":"have","start":2503580,"end":2503740,"confidence":0.9995117,"speaker":"A"},{"text":"one.","start":2503740,"end":2504020,"confidence":0.9863281,"speaker":"A"},{"text":"I","start":2504020,"end":2504260,"confidence":0.99316406,"speaker":"A"},{"text":"can","start":2504260,"end":2504420,"confidence":0.9458008,"speaker":"A"},{"text":"show","start":2504420,"end":2504580,"confidence":0.9995117,"speaker":"A"},{"text":"you.","start":2504580,"end":2504860,"confidence":0.9970703,"speaker":"A"},{"text":"Oh,","start":2505980,"end":2506180,"confidence":0.8977051,"speaker":"A"},{"text":"there's","start":2506180,"end":2506460,"confidence":0.91503906,"speaker":"A"},{"text":"one","start":2506460,"end":2506700,"confidence":0.99853516,"speaker":"A"},{"text":"scheduled.","start":2506700,"end":2507420,"confidence":0.97436523,"speaker":"A"},{"text":"Yeah,","start":2510060,"end":2510460,"confidence":0.97347003,"speaker":"A"},{"text":"here","start":2510460,"end":2510660,"confidence":0.9995117,"speaker":"A"},{"text":"we","start":2510660,"end":2510780,"confidence":1,"speaker":"A"},{"text":"go.","start":2510780,"end":2511020,"confidence":0.9995117,"speaker":"A"},{"text":"So","start":2511260,"end":2511660,"confidence":0.8173828,"speaker":"A"},{"text":"there's","start":2512060,"end":2512700,"confidence":0.9090169,"speaker":"A"},{"text":"57","start":2513100,"end":2513700,"confidence":0.99829,"speaker":"A"},{"text":"new","start":2513700,"end":2514060,"confidence":0.98291016,"speaker":"A"},{"text":"restore","start":2514060,"end":2514580,"confidence":0.84936523,"speaker":"A"},{"text":"images","start":2514580,"end":2514980,"confidence":0.9980469,"speaker":"A"},{"text":"created,","start":2514980,"end":2515580,"confidence":0.9970703,"speaker":"A"},{"text":"177","start":2516300,"end":2517500,"confidence":0.95771,"speaker":"A"},{"text":"updated.","start":2517660,"end":2518300,"confidence":0.9980469,"speaker":"A"},{"text":"234","start":2518780,"end":2519900,"confidence":0.93447,"speaker":"A"},{"text":"total.","start":2519980,"end":2520380,"confidence":0.9995117,"speaker":"A"},{"text":"No","start":2521420,"end":2521740,"confidence":0.9970703,"speaker":"A"},{"text":"operations","start":2521740,"end":2522300,"confidence":0.9987793,"speaker":"A"},{"text":"failed.","start":2522380,"end":2523020,"confidence":0.9992676,"speaker":"A"},{"text":"I","start":2523100,"end":2523380,"confidence":0.9916992,"speaker":"A"},{"text":"also","start":2523380,"end":2523580,"confidence":0.99902344,"speaker":"A"},{"text":"store","start":2523580,"end":2523900,"confidence":0.77490234,"speaker":"A"},{"text":"Xcode","start":2523900,"end":2524340,"confidence":0.89245605,"speaker":"A"},{"text":"versions","start":2524340,"end":2524700,"confidence":0.9970703,"speaker":"A"},{"text":"and","start":2524700,"end":2524980,"confidence":0.9370117,"speaker":"A"},{"text":"Swift","start":2524980,"end":2525420,"confidence":0.9921875,"speaker":"A"},{"text":"versions.","start":2525420,"end":2525900,"confidence":0.9975586,"speaker":"A"},{"text":"Those","start":2526780,"end":2527100,"confidence":0.99853516,"speaker":"A"},{"text":"get","start":2527100,"end":2527300,"confidence":0.99902344,"speaker":"A"},{"text":"stored","start":2527300,"end":2527620,"confidence":0.99853516,"speaker":"A"},{"text":"as","start":2527620,"end":2527780,"confidence":0.9995117,"speaker":"A"},{"text":"well.","start":2527780,"end":2528060,"confidence":0.9995117,"speaker":"A"},{"text":"Had","start":2529420,"end":2529700,"confidence":0.89697266,"speaker":"A"},{"text":"to","start":2529700,"end":2529860,"confidence":0.9736328,"speaker":"A"},{"text":"rebuild","start":2529860,"end":2530180,"confidence":0.9995117,"speaker":"A"},{"text":"it,","start":2530180,"end":2530460,"confidence":0.9975586,"speaker":"A"},{"text":"but","start":2530630,"end":2530790,"confidence":0.99902344,"speaker":"A"},{"text":"here","start":2530790,"end":2531070,"confidence":1,"speaker":"A"},{"text":"is","start":2531070,"end":2531310,"confidence":0.99902344,"speaker":"A"},{"text":"the","start":2531310,"end":2531510,"confidence":1,"speaker":"A"},{"text":"results.","start":2531510,"end":2531830,"confidence":0.98046875,"speaker":"A"},{"text":"I'm","start":2533750,"end":2534070,"confidence":0.9995117,"speaker":"A"},{"text":"not","start":2534070,"end":2534190,"confidence":0.9995117,"speaker":"A"},{"text":"going","start":2534190,"end":2534310,"confidence":0.9140625,"speaker":"A"},{"text":"to","start":2534310,"end":2534390,"confidence":0.9995117,"speaker":"A"},{"text":"pull","start":2534390,"end":2534590,"confidence":0.99975586,"speaker":"A"},{"text":"that","start":2534590,"end":2534750,"confidence":0.99853516,"speaker":"A"},{"text":"up,","start":2534750,"end":2535030,"confidence":0.9995117,"speaker":"A"},{"text":"but","start":2535830,"end":2536110,"confidence":0.9995117,"speaker":"A"},{"text":"it's","start":2536110,"end":2536350,"confidence":0.9944661,"speaker":"A"},{"text":"essentially","start":2536350,"end":2536950,"confidence":0.9980469,"speaker":"A"},{"text":"updated","start":2537270,"end":2537750,"confidence":0.99853516,"speaker":"A"},{"text":"my","start":2537750,"end":2537990,"confidence":0.99609375,"speaker":"A"},{"text":"CloudKit","start":2537990,"end":2538710,"confidence":0.9953613,"speaker":"A"},{"text":"database","start":2538790,"end":2539510,"confidence":0.99902344,"speaker":"A"},{"text":"and","start":2542070,"end":2542470,"confidence":0.99658203,"speaker":"A"},{"text":"that's","start":2542550,"end":2542950,"confidence":0.9998372,"speaker":"A"},{"text":"all","start":2542950,"end":2543070,"confidence":0.9995117,"speaker":"A"},{"text":"in","start":2543070,"end":2543190,"confidence":0.9892578,"speaker":"A"},{"text":"the","start":2543190,"end":2543310,"confidence":0.99902344,"speaker":"A"},{"text":"public","start":2543310,"end":2543510,"confidence":1,"speaker":"A"},{"text":"database.","start":2543510,"end":2544030,"confidence":0.9991862,"speaker":"A"},{"text":"And","start":2544030,"end":2544150,"confidence":0.9980469,"speaker":"A"},{"text":"then","start":2544150,"end":2544390,"confidence":0.9980469,"speaker":"A"},{"text":"maybe","start":2545110,"end":2545470,"confidence":0.99975586,"speaker":"A"},{"text":"even","start":2545470,"end":2545670,"confidence":0.9995117,"speaker":"A"},{"text":"by","start":2545670,"end":2545870,"confidence":0.9995117,"speaker":"A"},{"text":"the","start":2545870,"end":2546030,"confidence":0.9995117,"speaker":"A"},{"text":"time","start":2546030,"end":2546190,"confidence":1,"speaker":"A"},{"text":"I","start":2546190,"end":2546310,"confidence":0.99560547,"speaker":"A"},{"text":"present","start":2546310,"end":2546550,"confidence":0.9995117,"speaker":"A"},{"text":"this,","start":2546550,"end":2546869,"confidence":0.9995117,"speaker":"A"},{"text":"I'll","start":2546869,"end":2547110,"confidence":0.99902344,"speaker":"A"},{"text":"have","start":2547110,"end":2547310,"confidence":0.99853516,"speaker":"A"},{"text":"a","start":2547310,"end":2547550,"confidence":0.97314453,"speaker":"A"},{"text":"working","start":2547550,"end":2547830,"confidence":0.99902344,"speaker":"A"},{"text":"example","start":2547830,"end":2548350,"confidence":0.9814453,"speaker":"A"},{"text":"in","start":2548350,"end":2548510,"confidence":0.7578125,"speaker":"A"},{"text":"Bushel","start":2548510,"end":2548950,"confidence":0.9241536,"speaker":"A"},{"text":"with","start":2548950,"end":2549150,"confidence":1,"speaker":"A"},{"text":"that","start":2549150,"end":2549390,"confidence":0.9975586,"speaker":"A"},{"text":"example","start":2549390,"end":2549910,"confidence":0.9869792,"speaker":"A"},{"text":"working,","start":2549910,"end":2550230,"confidence":0.99902344,"speaker":"A"},{"text":"which","start":2550630,"end":2550910,"confidence":0.93310547,"speaker":"A"},{"text":"would","start":2550910,"end":2551070,"confidence":0.9277344,"speaker":"A"},{"text":"be","start":2551070,"end":2551230,"confidence":0.9995117,"speaker":"A"},{"text":"awesome.","start":2551230,"end":2551670,"confidence":0.99886066,"speaker":"A"},{"text":"Celestra,","start":2552870,"end":2553750,"confidence":0.7898763,"speaker":"A"},{"text":"same","start":2553990,"end":2554310,"confidence":0.99853516,"speaker":"A"},{"text":"idea.","start":2554310,"end":2554870,"confidence":0.998291,"speaker":"A"},{"text":"So","start":2555030,"end":2555310,"confidence":0.9970703,"speaker":"A"},{"text":"this","start":2555310,"end":2555470,"confidence":0.9916992,"speaker":"A"},{"text":"looks","start":2555470,"end":2555670,"confidence":0.99975586,"speaker":"A"},{"text":"like","start":2555670,"end":2555790,"confidence":0.9995117,"speaker":"A"},{"text":"it","start":2555790,"end":2555910,"confidence":0.9824219,"speaker":"A"},{"text":"was","start":2555910,"end":2555990,"confidence":0.9975586,"speaker":"A"},{"text":"a","start":2555990,"end":2556110,"confidence":0.80810547,"speaker":"A"},{"text":"RSS","start":2556110,"end":2556630,"confidence":0.72924805,"speaker":"A"},{"text":"update.","start":2556630,"end":2557190,"confidence":0.9975586,"speaker":"A"},{"text":"We","start":2558910,"end":2559030,"confidence":0.9663086,"speaker":"A"},{"text":"get","start":2559030,"end":2559150,"confidence":0.5415039,"speaker":"A"},{"text":"the","start":2559150,"end":2559270,"confidence":0.9970703,"speaker":"A"},{"text":"workflow","start":2559270,"end":2559790,"confidence":0.9992676,"speaker":"A"},{"text":"file","start":2559790,"end":2560190,"confidence":0.79589844,"speaker":"A"},{"text":"and.","start":2562510,"end":2562830,"confidence":0.8984375,"speaker":"A"},{"text":"Oh,","start":2562830,"end":2563150,"confidence":0.78930664,"speaker":"A"},{"text":"sorry,","start":2563150,"end":2563430,"confidence":0.9995117,"speaker":"A"},{"text":"I","start":2563430,"end":2563590,"confidence":0.99902344,"speaker":"A"},{"text":"should","start":2563590,"end":2563830,"confidence":0.9995117,"speaker":"A"},{"text":"point","start":2563830,"end":2564070,"confidence":1,"speaker":"A"},{"text":"out,","start":2564070,"end":2564270,"confidence":1,"speaker":"A"},{"text":"because","start":2564270,"end":2564470,"confidence":0.96191406,"speaker":"A"},{"text":"you're","start":2564470,"end":2564670,"confidence":0.9991862,"speaker":"A"},{"text":"probably","start":2564670,"end":2564870,"confidence":1,"speaker":"A"},{"text":"wondering","start":2564870,"end":2565270,"confidence":0.99121094,"speaker":"A"},{"text":"where","start":2565270,"end":2565510,"confidence":0.9995117,"speaker":"A"},{"text":"is","start":2565510,"end":2565670,"confidence":0.88183594,"speaker":"A"},{"text":"all","start":2565670,"end":2565830,"confidence":0.99121094,"speaker":"A"},{"text":"these.","start":2565830,"end":2566110,"confidence":0.8798828,"speaker":"A"},{"text":"The","start":2566110,"end":2566390,"confidence":0.8417969,"speaker":"A"},{"text":"stuff","start":2566390,"end":2566710,"confidence":0.99853516,"speaker":"A"},{"text":"all","start":2566710,"end":2566950,"confidence":0.9892578,"speaker":"A"},{"text":"these","start":2566950,"end":2567110,"confidence":0.7866211,"speaker":"A"},{"text":"secrets","start":2567110,"end":2567510,"confidence":0.97875977,"speaker":"A"},{"text":"stored?","start":2567510,"end":2567870,"confidence":0.98657227,"speaker":"A"},{"text":"Yes,","start":2567870,"end":2568150,"confidence":0.99975586,"speaker":"A"},{"text":"they","start":2568150,"end":2568310,"confidence":0.99902344,"speaker":"A"},{"text":"are","start":2568310,"end":2568510,"confidence":0.99902344,"speaker":"A"},{"text":"stored","start":2568510,"end":2568990,"confidence":0.99731445,"speaker":"A"},{"text":"in","start":2569790,"end":2570150,"confidence":0.9765625,"speaker":"A"},{"text":"Actions","start":2570150,"end":2570830,"confidence":0.9909668,"speaker":"A"},{"text":"secrets","start":2570990,"end":2571790,"confidence":0.998291,"speaker":"A"},{"text":"right","start":2572430,"end":2572750,"confidence":0.99853516,"speaker":"A"},{"text":"here.","start":2572750,"end":2573070,"confidence":0.9995117,"speaker":"A"},{"text":"So","start":2573310,"end":2573589,"confidence":0.94384766,"speaker":"A"},{"text":"we","start":2573589,"end":2573750,"confidence":1,"speaker":"A"},{"text":"have","start":2573750,"end":2573910,"confidence":1,"speaker":"A"},{"text":"our","start":2573910,"end":2574070,"confidence":0.8671875,"speaker":"A"},{"text":"private","start":2574070,"end":2574310,"confidence":0.9995117,"speaker":"A"},{"text":"key","start":2574310,"end":2574670,"confidence":0.9980469,"speaker":"A"},{"text":"ID","start":2575310,"end":2575710,"confidence":0.8774414,"speaker":"A"},{"text":"API","start":2576510,"end":2577070,"confidence":0.98535156,"speaker":"A"},{"text":"key","start":2577070,"end":2577390,"confidence":0.9970703,"speaker":"A"},{"text":"from","start":2577790,"end":2578190,"confidence":0.9995117,"speaker":"A"},{"text":"Virtual","start":2578190,"end":2578670,"confidence":0.99975586,"speaker":"A"},{"text":"Buddy.","start":2578670,"end":2579150,"confidence":0.97786456,"speaker":"A"},{"text":"So","start":2579550,"end":2579950,"confidence":0.9667969,"speaker":"A"},{"text":"that's","start":2580030,"end":2580430,"confidence":0.99625653,"speaker":"A"},{"text":"all","start":2580430,"end":2580550,"confidence":0.98779297,"speaker":"A"},{"text":"stored","start":2580550,"end":2580950,"confidence":0.9921875,"speaker":"A"},{"text":"there.","start":2580950,"end":2581230,"confidence":0.99658203,"speaker":"A"},{"text":"Here","start":2581870,"end":2582270,"confidence":0.99853516,"speaker":"A"},{"text":"is","start":2582350,"end":2582750,"confidence":0.9975586,"speaker":"A"},{"text":"Celestra.","start":2583150,"end":2583950,"confidence":0.8902995,"speaker":"A"},{"text":"It's","start":2584270,"end":2584710,"confidence":0.99886066,"speaker":"A"},{"text":"for","start":2584710,"end":2584910,"confidence":0.99902344,"speaker":"A"},{"text":"updating","start":2584910,"end":2585350,"confidence":0.9995117,"speaker":"A"},{"text":"RSS","start":2585350,"end":2585830,"confidence":0.9616699,"speaker":"A"},{"text":"feeds.","start":2585830,"end":2586350,"confidence":0.9967448,"speaker":"A"},{"text":"So","start":2587050,"end":2587130,"confidence":0.97216797,"speaker":"A"},{"text":"it","start":2587130,"end":2587210,"confidence":0.9663086,"speaker":"A"},{"text":"just","start":2587210,"end":2587370,"confidence":0.9951172,"speaker":"A"},{"text":"basically","start":2587370,"end":2587810,"confidence":0.99975586,"speaker":"A"},{"text":"goes","start":2587810,"end":2588170,"confidence":0.9995117,"speaker":"A"},{"text":"through.","start":2588170,"end":2588490,"confidence":0.9995117,"speaker":"A"},{"text":"You","start":2588570,"end":2588810,"confidence":0.9995117,"speaker":"A"},{"text":"can","start":2588810,"end":2588930,"confidence":0.9995117,"speaker":"A"},{"text":"look","start":2588930,"end":2589090,"confidence":1,"speaker":"A"},{"text":"at","start":2589090,"end":2589210,"confidence":1,"speaker":"A"},{"text":"the","start":2589210,"end":2589290,"confidence":0.9951172,"speaker":"A"},{"text":"Swift","start":2589290,"end":2589610,"confidence":0.99902344,"speaker":"A"},{"text":"code","start":2589610,"end":2589930,"confidence":0.976888,"speaker":"A"},{"text":"it","start":2589930,"end":2590130,"confidence":0.9995117,"speaker":"A"},{"text":"goes","start":2590130,"end":2590370,"confidence":0.9995117,"speaker":"A"},{"text":"through,","start":2590370,"end":2590610,"confidence":0.9995117,"speaker":"A"},{"text":"pulls","start":2590610,"end":2590970,"confidence":0.97249347,"speaker":"A"},{"text":"RSS","start":2590970,"end":2591370,"confidence":0.98217773,"speaker":"A"},{"text":"feeds","start":2591370,"end":2591890,"confidence":0.9975586,"speaker":"A"},{"text":"and","start":2591890,"end":2592090,"confidence":0.9975586,"speaker":"A"},{"text":"updates","start":2592090,"end":2592650,"confidence":0.9995117,"speaker":"A"},{"text":"them","start":2593050,"end":2593370,"confidence":0.98876953,"speaker":"A"},{"text":"into","start":2593370,"end":2593650,"confidence":0.9995117,"speaker":"A"},{"text":"a","start":2593650,"end":2593850,"confidence":0.9970703,"speaker":"A"},{"text":"CloudKit","start":2593850,"end":2594490,"confidence":0.9980469,"speaker":"A"},{"text":"record","start":2595530,"end":2595930,"confidence":0.99902344,"speaker":"A"},{"text":"or","start":2596410,"end":2596810,"confidence":0.9975586,"speaker":"A"},{"text":"what","start":2596890,"end":2597130,"confidence":0.9321289,"speaker":"A"},{"text":"do","start":2597130,"end":2597210,"confidence":0.8364258,"speaker":"A"},{"text":"you","start":2597210,"end":2597290,"confidence":0.9980469,"speaker":"A"},{"text":"call","start":2597290,"end":2597370,"confidence":1,"speaker":"A"},{"text":"it?","start":2597370,"end":2597490,"confidence":0.9951172,"speaker":"A"},{"text":"Yeah,","start":2597490,"end":2597730,"confidence":0.9558919,"speaker":"A"},{"text":"record","start":2597730,"end":2598010,"confidence":0.99853516,"speaker":"A"},{"text":"type.","start":2598010,"end":2598490,"confidence":0.9250488,"speaker":"A"},{"text":"And","start":2599850,"end":2600130,"confidence":0.9638672,"speaker":"A"},{"text":"I","start":2600130,"end":2600290,"confidence":0.9946289,"speaker":"A"},{"text":"of","start":2600290,"end":2600410,"confidence":0.64501953,"speaker":"A"},{"text":"course","start":2600410,"end":2600570,"confidence":0.9995117,"speaker":"A"},{"text":"try","start":2600570,"end":2600770,"confidence":0.9506836,"speaker":"A"},{"text":"to","start":2600770,"end":2600890,"confidence":0.9995117,"speaker":"A"},{"text":"do","start":2600890,"end":2600970,"confidence":1,"speaker":"A"},{"text":"it","start":2600970,"end":2601050,"confidence":0.9980469,"speaker":"A"},{"text":"in","start":2601050,"end":2601130,"confidence":0.98876953,"speaker":"A"},{"text":"such","start":2601130,"end":2601250,"confidence":1,"speaker":"A"},{"text":"a","start":2601250,"end":2601370,"confidence":0.96777344,"speaker":"A"},{"text":"way","start":2601370,"end":2601530,"confidence":1,"speaker":"A"},{"text":"not","start":2601530,"end":2601730,"confidence":0.99365234,"speaker":"A"},{"text":"to","start":2601730,"end":2601890,"confidence":0.9980469,"speaker":"A"},{"text":"hammer","start":2601890,"end":2602210,"confidence":0.9998372,"speaker":"A"},{"text":"people,","start":2602210,"end":2602490,"confidence":0.9995117,"speaker":"A"},{"text":"but","start":2602970,"end":2603370,"confidence":0.9902344,"speaker":"A"},{"text":"same","start":2603370,"end":2603690,"confidence":0.9941406,"speaker":"A"},{"text":"idea,","start":2603690,"end":2604170,"confidence":0.9914551,"speaker":"A"},{"text":"yeah,","start":2607050,"end":2607410,"confidence":0.96761066,"speaker":"A"},{"text":"it","start":2607410,"end":2607570,"confidence":0.99902344,"speaker":"A"},{"text":"goes","start":2607570,"end":2607770,"confidence":1,"speaker":"A"},{"text":"ahead","start":2607770,"end":2608010,"confidence":0.99853516,"speaker":"A"},{"text":"and","start":2608010,"end":2608330,"confidence":0.9921875,"speaker":"A"},{"text":"it","start":2608330,"end":2608570,"confidence":0.98828125,"speaker":"A"},{"text":"runs","start":2608570,"end":2609130,"confidence":0.9995117,"speaker":"A"},{"text":"the","start":2610330,"end":2610610,"confidence":0.9995117,"speaker":"A"},{"text":"binary","start":2610610,"end":2611210,"confidence":0.9991862,"speaker":"A"},{"text":"it","start":2611210,"end":2611530,"confidence":0.9711914,"speaker":"A"},{"text":"updates","start":2611530,"end":2612010,"confidence":0.9992676,"speaker":"A"},{"text":"and","start":2612170,"end":2612410,"confidence":0.98828125,"speaker":"A"},{"text":"then","start":2612410,"end":2612570,"confidence":0.99902344,"speaker":"A"},{"text":"I","start":2612570,"end":2612770,"confidence":0.9995117,"speaker":"A"},{"text":"also","start":2612770,"end":2612970,"confidence":0.9995117,"speaker":"A"},{"text":"have","start":2612970,"end":2613290,"confidence":0.99902344,"speaker":"A"},{"text":"like","start":2613290,"end":2613650,"confidence":0.9321289,"speaker":"A"},{"text":"actual","start":2613650,"end":2614170,"confidence":0.99853516,"speaker":"A"},{"text":"parameters","start":2615370,"end":2615890,"confidence":0.99902344,"speaker":"A"},{"text":"that","start":2615890,"end":2616010,"confidence":0.99902344,"speaker":"A"},{"text":"I","start":2616010,"end":2616130,"confidence":0.9995117,"speaker":"A"},{"text":"take","start":2616130,"end":2616330,"confidence":1,"speaker":"A"},{"text":"to","start":2616330,"end":2616570,"confidence":0.97314453,"speaker":"A"},{"text":"to","start":2616570,"end":2616810,"confidence":0.9995117,"speaker":"A"},{"text":"filter","start":2616810,"end":2617170,"confidence":0.9663086,"speaker":"A"},{"text":"out,","start":2617170,"end":2617410,"confidence":1,"speaker":"A"},{"text":"like","start":2617410,"end":2617610,"confidence":0.99658203,"speaker":"A"},{"text":"which","start":2617610,"end":2617890,"confidence":0.99902344,"speaker":"A"},{"text":"RSS","start":2617890,"end":2618410,"confidence":0.99853516,"speaker":"A"},{"text":"feeds","start":2618410,"end":2618970,"confidence":0.9991862,"speaker":"A"},{"text":"are","start":2619290,"end":2619610,"confidence":0.96240234,"speaker":"A"},{"text":"high","start":2619610,"end":2619810,"confidence":1,"speaker":"A"},{"text":"priority","start":2619810,"end":2620170,"confidence":1,"speaker":"A"},{"text":"and","start":2620170,"end":2620330,"confidence":0.92626953,"speaker":"A"},{"text":"which","start":2620330,"end":2620450,"confidence":1,"speaker":"A"},{"text":"ones","start":2620450,"end":2620690,"confidence":0.9995117,"speaker":"A"},{"text":"aren't","start":2620690,"end":2621010,"confidence":0.99768066,"speaker":"A"},{"text":"based","start":2621010,"end":2621170,"confidence":1,"speaker":"A"},{"text":"on","start":2621170,"end":2621330,"confidence":1,"speaker":"A"},{"text":"the","start":2621330,"end":2621490,"confidence":0.99365234,"speaker":"A"},{"text":"audience","start":2621490,"end":2621770,"confidence":0.99853516,"speaker":"A"},{"text":"and","start":2621770,"end":2621970,"confidence":0.9975586,"speaker":"A"},{"text":"etc.","start":2621970,"end":2622650,"confidence":0.90723,"speaker":"A"},{"text":"So","start":2622650,"end":2623050,"confidence":0.9946289,"speaker":"A"},{"text":"yeah,","start":2623850,"end":2624330,"confidence":0.95377606,"speaker":"A"},{"text":"so","start":2624890,"end":2625170,"confidence":0.99853516,"speaker":"A"},{"text":"that's","start":2625170,"end":2625450,"confidence":0.9946289,"speaker":"A"},{"text":"deployment.","start":2625450,"end":2626170,"confidence":0.9991862,"speaker":"A"},{"text":"That's","start":2627050,"end":2627450,"confidence":0.9998372,"speaker":"A"},{"text":"how","start":2627450,"end":2627530,"confidence":1,"speaker":"A"},{"text":"you","start":2627530,"end":2627650,"confidence":0.9995117,"speaker":"A"},{"text":"can","start":2627650,"end":2627770,"confidence":1,"speaker":"A"},{"text":"get","start":2627770,"end":2627890,"confidence":1,"speaker":"A"},{"text":"that","start":2627890,"end":2628090,"confidence":1,"speaker":"A"},{"text":"working.","start":2628090,"end":2628410,"confidence":0.9995117,"speaker":"A"},{"text":"There's","start":2628810,"end":2629250,"confidence":0.9996745,"speaker":"A"},{"text":"weird","start":2629250,"end":2629490,"confidence":1,"speaker":"A"},{"text":"stuff","start":2629490,"end":2629690,"confidence":1,"speaker":"A"},{"text":"with","start":2629690,"end":2629850,"confidence":0.99609375,"speaker":"A"},{"text":"cloud","start":2629850,"end":2630290,"confidence":0.8815918,"speaker":"A"},{"text":"with","start":2630290,"end":2630650,"confidence":0.9873047,"speaker":"A"},{"text":"GitHub","start":2630810,"end":2631530,"confidence":0.99853516,"speaker":"A"},{"text":"that","start":2632730,"end":2633130,"confidence":0.9975586,"speaker":"A"},{"text":"I've","start":2633690,"end":2634010,"confidence":1,"speaker":"A"},{"text":"noticed.","start":2634010,"end":2634330,"confidence":0.99869794,"speaker":"A"},{"text":"If","start":2634330,"end":2634530,"confidence":0.9975586,"speaker":"A"},{"text":"you","start":2634530,"end":2634730,"confidence":0.9995117,"speaker":"A"},{"text":"haven't","start":2634730,"end":2635010,"confidence":0.9984131,"speaker":"A"},{"text":"updated","start":2635010,"end":2635370,"confidence":0.9995117,"speaker":"A"},{"text":"it","start":2635370,"end":2635610,"confidence":0.96240234,"speaker":"A"},{"text":"in","start":2635610,"end":2635810,"confidence":0.99902344,"speaker":"A"},{"text":"a","start":2635810,"end":2635970,"confidence":0.99560547,"speaker":"A"},{"text":"while,","start":2635970,"end":2636250,"confidence":0.9995117,"speaker":"A"},{"text":"it","start":2636250,"end":2636530,"confidence":1,"speaker":"A"},{"text":"doesn't","start":2636530,"end":2636770,"confidence":0.9998372,"speaker":"A"},{"text":"run","start":2636770,"end":2636970,"confidence":0.99853516,"speaker":"A"},{"text":"these","start":2636970,"end":2637210,"confidence":0.96777344,"speaker":"A"},{"text":"cron","start":2637210,"end":2637490,"confidence":0.90527344,"speaker":"A"},{"text":"jobs.","start":2637490,"end":2637770,"confidence":0.99072266,"speaker":"A"},{"text":"So","start":2637770,"end":2637850,"confidence":0.9951172,"speaker":"A"},{"text":"I","start":2637850,"end":2637930,"confidence":1,"speaker":"A"},{"text":"need","start":2637930,"end":2638050,"confidence":1,"speaker":"A"},{"text":"to","start":2638050,"end":2638170,"confidence":0.99902344,"speaker":"A"},{"text":"figure","start":2638170,"end":2638330,"confidence":0.99975586,"speaker":"A"},{"text":"out","start":2638330,"end":2638490,"confidence":0.98828125,"speaker":"A"},{"text":"a","start":2638490,"end":2638690,"confidence":0.89941406,"speaker":"A"},{"text":"how","start":2638690,"end":2638850,"confidence":0.99853516,"speaker":"A"},{"text":"to","start":2638850,"end":2638970,"confidence":0.9995117,"speaker":"A"},{"text":"get","start":2638970,"end":2639050,"confidence":0.9995117,"speaker":"A"},{"text":"around","start":2639050,"end":2639210,"confidence":0.99853516,"speaker":"A"},{"text":"it","start":2639210,"end":2639410,"confidence":0.9238281,"speaker":"A"},{"text":"or","start":2639410,"end":2639570,"confidence":0.9995117,"speaker":"A"},{"text":"find","start":2639570,"end":2639730,"confidence":0.9995117,"speaker":"A"},{"text":"another","start":2639730,"end":2640010,"confidence":0.9477539,"speaker":"A"},{"text":"service","start":2640090,"end":2640450,"confidence":0.9819336,"speaker":"A"},{"text":"to","start":2640450,"end":2640650,"confidence":0.9970703,"speaker":"A"},{"text":"do","start":2640650,"end":2640730,"confidence":0.99902344,"speaker":"A"},{"text":"it.","start":2640730,"end":2640970,"confidence":0.9975586,"speaker":"A"},{"text":"This","start":2642830,"end":2642950,"confidence":0.9897461,"speaker":"A"},{"text":"is","start":2642950,"end":2643110,"confidence":0.9975586,"speaker":"A"},{"text":"all","start":2643110,"end":2643270,"confidence":0.9995117,"speaker":"A"},{"text":"free","start":2643270,"end":2643550,"confidence":1,"speaker":"A"},{"text":"because","start":2643630,"end":2644030,"confidence":0.9995117,"speaker":"A"},{"text":"it's","start":2644110,"end":2644590,"confidence":0.99934894,"speaker":"A"},{"text":"public","start":2644590,"end":2644870,"confidence":1,"speaker":"A"},{"text":"and","start":2644870,"end":2645230,"confidence":0.7548828,"speaker":"A"},{"text":"it","start":2646990,"end":2647310,"confidence":0.99902344,"speaker":"A"},{"text":"is","start":2647310,"end":2647550,"confidence":0.9995117,"speaker":"A"},{"text":"running","start":2647550,"end":2647870,"confidence":0.9987793,"speaker":"A"},{"text":"on","start":2647870,"end":2647990,"confidence":0.7963867,"speaker":"A"},{"text":"Ubuntu.","start":2647990,"end":2648590,"confidence":0.8631836,"speaker":"A"},{"text":"So","start":2648670,"end":2648910,"confidence":0.9980469,"speaker":"A"},{"text":"that's","start":2648910,"end":2649310,"confidence":0.99934894,"speaker":"A"},{"text":"really","start":2649310,"end":2649550,"confidence":1,"speaker":"A"},{"text":"great.","start":2649550,"end":2649870,"confidence":0.99902344,"speaker":"A"},{"text":"And","start":2652350,"end":2652750,"confidence":0.9838867,"speaker":"A"},{"text":"the","start":2652830,"end":2653110,"confidence":0.9995117,"speaker":"A"},{"text":"storage","start":2653110,"end":2653430,"confidence":1,"speaker":"A"},{"text":"on","start":2653430,"end":2653590,"confidence":0.9951172,"speaker":"A"},{"text":"CloudKit","start":2653590,"end":2654150,"confidence":0.94189453,"speaker":"A"},{"text":"is","start":2654150,"end":2654310,"confidence":0.99902344,"speaker":"A"},{"text":"dirt","start":2654310,"end":2654590,"confidence":0.8517253,"speaker":"A"},{"text":"cheap,","start":2654590,"end":2654990,"confidence":0.8378906,"speaker":"A"},{"text":"which","start":2655390,"end":2655670,"confidence":0.99902344,"speaker":"A"},{"text":"is","start":2655670,"end":2655830,"confidence":1,"speaker":"A"},{"text":"even","start":2655830,"end":2656070,"confidence":1,"speaker":"A"},{"text":"more","start":2656070,"end":2656310,"confidence":1,"speaker":"A"},{"text":"awesome.","start":2656310,"end":2656830,"confidence":0.99886066,"speaker":"A"},{"text":"Sorry,","start":2660030,"end":2660590,"confidence":0.99593097,"speaker":"A"},{"text":"let's","start":2660990,"end":2661350,"confidence":0.89501953,"speaker":"A"},{"text":"see","start":2661350,"end":2661550,"confidence":0.9848633,"speaker":"A"},{"text":"what","start":2661550,"end":2661750,"confidence":0.99609375,"speaker":"A"},{"text":"else.","start":2661750,"end":2662110,"confidence":0.99975586,"speaker":"A"},{"text":"I","start":2663630,"end":2663870,"confidence":0.9682617,"speaker":"A"},{"text":"just","start":2663870,"end":2663990,"confidence":0.9824219,"speaker":"A"},{"text":"want","start":2663990,"end":2664110,"confidence":0.75878906,"speaker":"A"},{"text":"to","start":2664110,"end":2664230,"confidence":0.7807617,"speaker":"A"},{"text":"make","start":2664230,"end":2664350,"confidence":0.9995117,"speaker":"A"},{"text":"sure","start":2664350,"end":2664430,"confidence":1,"speaker":"A"},{"text":"I","start":2664430,"end":2664550,"confidence":0.98779297,"speaker":"A"},{"text":"covered","start":2664550,"end":2664870,"confidence":0.99975586,"speaker":"A"},{"text":"all","start":2664870,"end":2665070,"confidence":0.99902344,"speaker":"A"},{"text":"my","start":2665070,"end":2665390,"confidence":0.9970703,"speaker":"A"},{"text":"slides.","start":2665630,"end":2666150,"confidence":0.99975586,"speaker":"A"},{"text":"The","start":2666150,"end":2666390,"confidence":0.9995117,"speaker":"A"},{"text":"last","start":2666390,"end":2666590,"confidence":1,"speaker":"A"},{"text":"thing","start":2666590,"end":2666790,"confidence":1,"speaker":"A"},{"text":"I'm","start":2666790,"end":2666990,"confidence":0.9980469,"speaker":"A"},{"text":"going","start":2666990,"end":2667070,"confidence":0.96777344,"speaker":"A"},{"text":"to","start":2667070,"end":2667150,"confidence":0.9995117,"speaker":"A"},{"text":"talk","start":2667150,"end":2667270,"confidence":1,"speaker":"A"},{"text":"about","start":2667270,"end":2667470,"confidence":0.9995117,"speaker":"A"},{"text":"is","start":2667470,"end":2667670,"confidence":0.9941406,"speaker":"A"},{"text":"just","start":2667670,"end":2667830,"confidence":0.9941406,"speaker":"A"},{"text":"what","start":2667830,"end":2667990,"confidence":0.99853516,"speaker":"A"},{"text":"are","start":2667990,"end":2668150,"confidence":0.99902344,"speaker":"A"},{"text":"my","start":2668150,"end":2668310,"confidence":1,"speaker":"A"},{"text":"plans?","start":2668310,"end":2668670,"confidence":0.92578125,"speaker":"A"},{"text":"Excuse","start":2670390,"end":2670750,"confidence":0.9793294,"speaker":"A"},{"text":"me.","start":2670750,"end":2671030,"confidence":1,"speaker":"A"},{"text":"So","start":2671510,"end":2671790,"confidence":0.9980469,"speaker":"A"},{"text":"I","start":2671790,"end":2671910,"confidence":0.9995117,"speaker":"A"},{"text":"don't","start":2671910,"end":2672070,"confidence":0.99934894,"speaker":"A"},{"text":"know","start":2672070,"end":2672150,"confidence":1,"speaker":"A"},{"text":"if","start":2672150,"end":2672230,"confidence":1,"speaker":"A"},{"text":"you","start":2672230,"end":2672390,"confidence":0.9995117,"speaker":"A"},{"text":"check.","start":2672390,"end":2672790,"confidence":0.7727051,"speaker":"A"},{"text":"Follow","start":2672790,"end":2673150,"confidence":0.9663086,"speaker":"A"},{"text":"me.","start":2673150,"end":2673390,"confidence":1,"speaker":"A"},{"text":"But","start":2673390,"end":2673550,"confidence":0.99853516,"speaker":"A"},{"text":"I","start":2673550,"end":2673710,"confidence":0.9995117,"speaker":"A"},{"text":"just","start":2673710,"end":2673910,"confidence":0.99902344,"speaker":"A"},{"text":"released.","start":2673910,"end":2674550,"confidence":0.99975586,"speaker":"A"},{"text":"I","start":2681910,"end":2682190,"confidence":0.98876953,"speaker":"A"},{"text":"just","start":2682190,"end":2682350,"confidence":1,"speaker":"A"},{"text":"released","start":2682350,"end":2682710,"confidence":0.99975586,"speaker":"A"},{"text":"Alpha","start":2682710,"end":2683150,"confidence":0.85091144,"speaker":"A"},{"text":"5","start":2683150,"end":2683430,"confidence":0.99414,"speaker":"A"},{"text":"that","start":2684310,"end":2684630,"confidence":1,"speaker":"A"},{"text":"has","start":2684630,"end":2684909,"confidence":0.9995117,"speaker":"A"},{"text":"lookup","start":2684909,"end":2685390,"confidence":0.89086914,"speaker":"A"},{"text":"zones,","start":2685390,"end":2685750,"confidence":0.9760742,"speaker":"A"},{"text":"fetch,","start":2685750,"end":2686150,"confidence":0.9900716,"speaker":"A"},{"text":"record","start":2686150,"end":2686430,"confidence":0.9995117,"speaker":"A"},{"text":"changes","start":2686430,"end":2686870,"confidence":0.99975586,"speaker":"A"},{"text":"and","start":2686870,"end":2687030,"confidence":0.6220703,"speaker":"A"},{"text":"upload","start":2687030,"end":2687430,"confidence":0.71809894,"speaker":"A"},{"text":"assets.","start":2687430,"end":2687990,"confidence":1,"speaker":"A"},{"text":"Upload","start":2688310,"end":2688750,"confidence":0.9840495,"speaker":"A"},{"text":"the","start":2688750,"end":2688910,"confidence":0.7114258,"speaker":"A"},{"text":"assets","start":2688910,"end":2689270,"confidence":0.99902344,"speaker":"A"},{"text":"is","start":2689270,"end":2689470,"confidence":0.9814453,"speaker":"A"},{"text":"pretty","start":2689470,"end":2689710,"confidence":1,"speaker":"A"},{"text":"awesome.","start":2689710,"end":2690150,"confidence":1,"speaker":"A"},{"text":"When","start":2690230,"end":2690510,"confidence":0.99902344,"speaker":"A"},{"text":"I","start":2690510,"end":2690670,"confidence":1,"speaker":"A"},{"text":"saw","start":2690670,"end":2690830,"confidence":1,"speaker":"A"},{"text":"that","start":2690830,"end":2691030,"confidence":0.9995117,"speaker":"A"},{"text":"work","start":2691030,"end":2691310,"confidence":0.99902344,"speaker":"A"},{"text":"because","start":2691310,"end":2691590,"confidence":1,"speaker":"A"},{"text":"I","start":2691590,"end":2691750,"confidence":0.9536133,"speaker":"A"},{"text":"was","start":2691750,"end":2691870,"confidence":0.9975586,"speaker":"A"},{"text":"like,","start":2691870,"end":2691990,"confidence":0.9980469,"speaker":"A"},{"text":"cool,","start":2691990,"end":2692190,"confidence":0.99853516,"speaker":"A"},{"text":"I","start":2692190,"end":2692310,"confidence":0.9951172,"speaker":"A"},{"text":"can","start":2692310,"end":2692470,"confidence":0.9970703,"speaker":"A"},{"text":"actually","start":2692470,"end":2692670,"confidence":0.9995117,"speaker":"A"},{"text":"upload","start":2692670,"end":2693030,"confidence":1,"speaker":"A"},{"text":"a","start":2693030,"end":2693150,"confidence":0.9951172,"speaker":"A"},{"text":"binary","start":2693150,"end":2693750,"confidence":0.99853516,"speaker":"A"},{"text":"to","start":2694630,"end":2694910,"confidence":0.96728516,"speaker":"A"},{"text":"CloudKit,","start":2694910,"end":2695510,"confidence":0.98046875,"speaker":"A"},{"text":"which","start":2695510,"end":2695710,"confidence":0.9995117,"speaker":"A"},{"text":"is","start":2695710,"end":2695830,"confidence":0.9995117,"speaker":"A"},{"text":"awesome.","start":2695830,"end":2696230,"confidence":0.9998372,"speaker":"A"},{"text":"We","start":2697310,"end":2697430,"confidence":0.99121094,"speaker":"A"},{"text":"got","start":2697430,"end":2697630,"confidence":0.9946289,"speaker":"A"},{"text":"query","start":2697630,"end":2697990,"confidence":0.9836426,"speaker":"A"},{"text":"filters","start":2697990,"end":2698470,"confidence":0.9889323,"speaker":"A"},{"text":"to","start":2698470,"end":2698630,"confidence":0.99853516,"speaker":"A"},{"text":"work","start":2698630,"end":2698790,"confidence":1,"speaker":"A"},{"text":"for","start":2698790,"end":2698950,"confidence":0.9980469,"speaker":"A"},{"text":"in","start":2698950,"end":2699150,"confidence":0.88183594,"speaker":"A"},{"text":"and","start":2699150,"end":2699310,"confidence":0.9741211,"speaker":"A"},{"text":"not","start":2699310,"end":2699510,"confidence":0.98339844,"speaker":"A"},{"text":"in,","start":2699510,"end":2699870,"confidence":0.8652344,"speaker":"A"},{"text":"so","start":2699870,"end":2700110,"confidence":0.99853516,"speaker":"A"},{"text":"you","start":2700110,"end":2700190,"confidence":0.99853516,"speaker":"A"},{"text":"could","start":2700190,"end":2700350,"confidence":0.95410156,"speaker":"A"},{"text":"do","start":2700350,"end":2700550,"confidence":1,"speaker":"A"},{"text":"that","start":2700550,"end":2700830,"confidence":0.9995117,"speaker":"A"},{"text":"I","start":2701470,"end":2701790,"confidence":0.99902344,"speaker":"A"},{"text":"have","start":2701790,"end":2702110,"confidence":0.9995117,"speaker":"A"},{"text":"plans","start":2702110,"end":2702630,"confidence":0.95043945,"speaker":"A"},{"text":"to","start":2702630,"end":2702750,"confidence":0.95166016,"speaker":"A"},{"text":"continue","start":2702750,"end":2702950,"confidence":0.9980469,"speaker":"A"},{"text":"working","start":2702950,"end":2703230,"confidence":0.9238281,"speaker":"A"},{"text":"on","start":2703230,"end":2703430,"confidence":0.99853516,"speaker":"A"},{"text":"this","start":2703430,"end":2703630,"confidence":0.99902344,"speaker":"A"},{"text":"because","start":2703630,"end":2703830,"confidence":0.9555664,"speaker":"A"},{"text":"I","start":2703830,"end":2703990,"confidence":0.9995117,"speaker":"A"},{"text":"think","start":2703990,"end":2704230,"confidence":0.99902344,"speaker":"A"},{"text":"there's","start":2704230,"end":2704710,"confidence":0.9991862,"speaker":"A"},{"text":"a","start":2704710,"end":2704830,"confidence":0.9995117,"speaker":"A"},{"text":"big","start":2704830,"end":2704990,"confidence":0.99902344,"speaker":"A"},{"text":"future","start":2704990,"end":2705270,"confidence":0.9970703,"speaker":"A"},{"text":"for","start":2705270,"end":2705510,"confidence":0.9995117,"speaker":"A"},{"text":"something","start":2705510,"end":2705750,"confidence":0.99560547,"speaker":"A"},{"text":"like","start":2705750,"end":2705990,"confidence":0.9995117,"speaker":"A"},{"text":"this","start":2705990,"end":2706190,"confidence":0.9995117,"speaker":"A"},{"text":"for","start":2706190,"end":2706390,"confidence":0.9995117,"speaker":"A"},{"text":"a","start":2706390,"end":2706510,"confidence":0.9995117,"speaker":"A"},{"text":"lot","start":2706510,"end":2706590,"confidence":1,"speaker":"A"},{"text":"of","start":2706590,"end":2706710,"confidence":0.9995117,"speaker":"A"},{"text":"people.","start":2706710,"end":2706990,"confidence":0.9995117,"speaker":"A"},{"text":"Yes,","start":2709150,"end":2709590,"confidence":0.9716797,"speaker":"A"},{"text":"you","start":2709590,"end":2709830,"confidence":0.9995117,"speaker":"A"},{"text":"can","start":2709830,"end":2709990,"confidence":0.93603516,"speaker":"A"},{"text":"technically","start":2709990,"end":2710350,"confidence":0.9992676,"speaker":"A"},{"text":"use","start":2710350,"end":2710590,"confidence":0.9995117,"speaker":"A"},{"text":"this","start":2710590,"end":2710790,"confidence":0.98095703,"speaker":"A"},{"text":"in","start":2710790,"end":2710950,"confidence":0.9633789,"speaker":"A"},{"text":"Android","start":2710950,"end":2711470,"confidence":0.99934894,"speaker":"A"},{"text":"or","start":2711470,"end":2711710,"confidence":0.9995117,"speaker":"A"},{"text":"Windows","start":2711710,"end":2712270,"confidence":0.9972331,"speaker":"A"},{"text":"because","start":2712670,"end":2713070,"confidence":0.9995117,"speaker":"A"},{"text":"the","start":2713230,"end":2713510,"confidence":0.9970703,"speaker":"A"},{"text":"Swift","start":2713510,"end":2713950,"confidence":0.998291,"speaker":"A"},{"text":"thing","start":2714270,"end":2714590,"confidence":0.99902344,"speaker":"A"},{"text":"does","start":2714590,"end":2714830,"confidence":0.9995117,"speaker":"A"},{"text":"compile","start":2714830,"end":2715190,"confidence":0.99487305,"speaker":"A"},{"text":"in","start":2715190,"end":2715350,"confidence":0.78271484,"speaker":"A"},{"text":"Android","start":2715350,"end":2715750,"confidence":0.99853516,"speaker":"A"},{"text":"and","start":2715750,"end":2715910,"confidence":0.72753906,"speaker":"A"},{"text":"Windows.","start":2715910,"end":2716230,"confidence":0.99934894,"speaker":"A"},{"text":"You","start":2716230,"end":2716350,"confidence":0.9970703,"speaker":"A"},{"text":"can","start":2716350,"end":2716430,"confidence":0.88623047,"speaker":"A"},{"text":"see","start":2716430,"end":2716550,"confidence":0.9995117,"speaker":"A"},{"text":"I","start":2716550,"end":2716670,"confidence":0.63378906,"speaker":"A"},{"text":"already","start":2716670,"end":2716830,"confidence":0.99560547,"speaker":"A"},{"text":"added","start":2716830,"end":2717110,"confidence":0.9819336,"speaker":"A"},{"text":"support","start":2717110,"end":2717430,"confidence":1,"speaker":"A"},{"text":"for","start":2717430,"end":2717670,"confidence":1,"speaker":"A"},{"text":"that.","start":2717670,"end":2717950,"confidence":0.9995117,"speaker":"A"},{"text":"This","start":2718430,"end":2718710,"confidence":0.99609375,"speaker":"A"},{"text":"is","start":2718710,"end":2718870,"confidence":0.9975586,"speaker":"A"},{"text":"the","start":2718870,"end":2719030,"confidence":0.88720703,"speaker":"A"},{"text":"support","start":2719030,"end":2719270,"confidence":0.9970703,"speaker":"A"},{"text":"I","start":2719270,"end":2719510,"confidence":0.99658203,"speaker":"A"},{"text":"recently","start":2719510,"end":2719790,"confidence":1,"speaker":"A"},{"text":"had.","start":2719870,"end":2720270,"confidence":0.9995117,"speaker":"A"},{"text":"And","start":2720750,"end":2721030,"confidence":0.9814453,"speaker":"A"},{"text":"then","start":2721030,"end":2721310,"confidence":0.99121094,"speaker":"A"},{"text":"we're.","start":2722120,"end":2722360,"confidence":0.77229816,"speaker":"A"},{"text":"I'm","start":2722360,"end":2722600,"confidence":0.9868164,"speaker":"A"},{"text":"just","start":2722600,"end":2722720,"confidence":0.9995117,"speaker":"A"},{"text":"kind","start":2722720,"end":2722840,"confidence":0.9946289,"speaker":"A"},{"text":"of","start":2722840,"end":2722960,"confidence":0.9370117,"speaker":"A"},{"text":"like","start":2722960,"end":2723200,"confidence":0.99609375,"speaker":"A"},{"text":"going","start":2723200,"end":2723480,"confidence":0.99902344,"speaker":"A"},{"text":"through","start":2723480,"end":2723720,"confidence":1,"speaker":"A"},{"text":"each","start":2723720,"end":2723920,"confidence":0.9995117,"speaker":"A"},{"text":"of","start":2723920,"end":2724040,"confidence":0.9995117,"speaker":"A"},{"text":"these","start":2724040,"end":2724280,"confidence":0.99902344,"speaker":"A"},{"text":"because","start":2724280,"end":2724680,"confidence":0.7866211,"speaker":"A"},{"text":"as","start":2724680,"end":2725000,"confidence":1,"speaker":"A"},{"text":"great","start":2725000,"end":2725240,"confidence":0.9951172,"speaker":"A"},{"text":"as","start":2725240,"end":2725480,"confidence":0.9946289,"speaker":"A"},{"text":"AI","start":2725480,"end":2725880,"confidence":0.8781738,"speaker":"A"},{"text":"is,","start":2725880,"end":2726160,"confidence":0.9946289,"speaker":"A"},{"text":"it's","start":2726160,"end":2726440,"confidence":0.9995117,"speaker":"A"},{"text":"not","start":2726440,"end":2726600,"confidence":0.9995117,"speaker":"A"},{"text":"perfect.","start":2726600,"end":2727000,"confidence":0.9840495,"speaker":"A"},{"text":"So","start":2727080,"end":2727480,"confidence":0.99853516,"speaker":"A"},{"text":"we're","start":2728040,"end":2728360,"confidence":0.99934894,"speaker":"A"},{"text":"just","start":2728360,"end":2728440,"confidence":1,"speaker":"A"},{"text":"kind","start":2728440,"end":2728560,"confidence":0.99365234,"speaker":"A"},{"text":"of","start":2728560,"end":2728680,"confidence":0.98828125,"speaker":"A"},{"text":"going","start":2728680,"end":2728880,"confidence":0.99365234,"speaker":"A"},{"text":"through","start":2728880,"end":2729120,"confidence":1,"speaker":"A"},{"text":"these","start":2729120,"end":2729400,"confidence":0.98779297,"speaker":"A"},{"text":"piece","start":2729720,"end":2730120,"confidence":0.9848633,"speaker":"A"},{"text":"by","start":2730120,"end":2730360,"confidence":0.99902344,"speaker":"A"},{"text":"piece","start":2730360,"end":2730760,"confidence":0.9983724,"speaker":"A"},{"text":"with","start":2730840,"end":2731120,"confidence":0.9995117,"speaker":"A"},{"text":"each","start":2731120,"end":2731400,"confidence":0.9995117,"speaker":"A"},{"text":"version","start":2731640,"end":2732080,"confidence":0.99975586,"speaker":"A"},{"text":"and","start":2732080,"end":2732240,"confidence":0.5917969,"speaker":"A"},{"text":"hammering","start":2732240,"end":2732560,"confidence":0.9977214,"speaker":"A"},{"text":"these","start":2732560,"end":2732760,"confidence":0.99609375,"speaker":"A"},{"text":"away","start":2732760,"end":2733080,"confidence":0.9980469,"speaker":"A"},{"text":"and","start":2735400,"end":2735720,"confidence":0.9951172,"speaker":"A"},{"text":"then","start":2735720,"end":2736040,"confidence":0.9975586,"speaker":"A"},{"text":"this","start":2736680,"end":2736960,"confidence":0.9980469,"speaker":"A"},{"text":"is","start":2736960,"end":2737120,"confidence":0.99365234,"speaker":"A"},{"text":"actually","start":2737120,"end":2737360,"confidence":0.9995117,"speaker":"A"},{"text":"done.","start":2737360,"end":2737640,"confidence":0.9980469,"speaker":"A"},{"text":"I","start":2737640,"end":2737840,"confidence":0.9995117,"speaker":"A"},{"text":"don't","start":2737840,"end":2738000,"confidence":0.98844403,"speaker":"A"},{"text":"even","start":2738000,"end":2738159,"confidence":0.99902344,"speaker":"A"},{"text":"know","start":2738159,"end":2738279,"confidence":1,"speaker":"A"},{"text":"why","start":2738279,"end":2738400,"confidence":0.99902344,"speaker":"A"},{"text":"that's","start":2738400,"end":2738680,"confidence":0.9995117,"speaker":"A"},{"text":"there.","start":2738680,"end":2738880,"confidence":0.99853516,"speaker":"A"},{"text":"But","start":2738880,"end":2739240,"confidence":0.99658203,"speaker":"A"},{"text":"yeah,","start":2739640,"end":2740160,"confidence":0.99934894,"speaker":"A"},{"text":"I","start":2740160,"end":2740400,"confidence":0.83203125,"speaker":"A"},{"text":"think","start":2740400,"end":2740680,"confidence":0.92529297,"speaker":"A"},{"text":"system","start":2740680,"end":2741080,"confidence":0.9995117,"speaker":"A"},{"text":"field","start":2741080,"end":2741480,"confidence":0.9916992,"speaker":"A"},{"text":"integration","start":2741640,"end":2742280,"confidence":0.93859863,"speaker":"A"},{"text":"might","start":2742280,"end":2742480,"confidence":0.9980469,"speaker":"A"},{"text":"already","start":2742480,"end":2742720,"confidence":0.9995117,"speaker":"A"},{"text":"be","start":2742720,"end":2742960,"confidence":1,"speaker":"A"},{"text":"there","start":2742960,"end":2743240,"confidence":1,"speaker":"A"},{"text":"and","start":2743400,"end":2743680,"confidence":0.9980469,"speaker":"A"},{"text":"there's","start":2743680,"end":2743960,"confidence":0.99853516,"speaker":"A"},{"text":"a","start":2743960,"end":2744040,"confidence":0.9995117,"speaker":"A"},{"text":"few","start":2744040,"end":2744160,"confidence":0.9995117,"speaker":"A"},{"text":"other","start":2744160,"end":2744400,"confidence":1,"speaker":"A"},{"text":"things.","start":2744400,"end":2744760,"confidence":0.9995117,"speaker":"A"},{"text":"Eventually","start":2745960,"end":2746520,"confidence":0.9992676,"speaker":"A"},{"text":"I'd","start":2746520,"end":2746800,"confidence":0.92122394,"speaker":"A"},{"text":"like","start":2746800,"end":2746960,"confidence":0.9995117,"speaker":"A"},{"text":"to","start":2746960,"end":2747160,"confidence":0.99902344,"speaker":"A"},{"text":"add","start":2747160,"end":2747480,"confidence":0.9975586,"speaker":"A"},{"text":"support.","start":2747880,"end":2748120,"confidence":0.9902344,"speaker":"A"},{"text":"So","start":2748200,"end":2748480,"confidence":0.99902344,"speaker":"A"},{"text":"there,","start":2748480,"end":2748720,"confidence":0.38134766,"speaker":"A"},{"text":"there's","start":2748720,"end":2749080,"confidence":0.9998372,"speaker":"A"},{"text":"a","start":2749080,"end":2749200,"confidence":0.9995117,"speaker":"A"},{"text":"whole","start":2749200,"end":2749440,"confidence":0.99975586,"speaker":"A"},{"text":"API","start":2749440,"end":2749880,"confidence":0.9975586,"speaker":"A"},{"text":"for","start":2749880,"end":2750120,"confidence":0.9975586,"speaker":"A"},{"text":"CloudKit","start":2750120,"end":2750760,"confidence":0.99609375,"speaker":"A"},{"text":"schema","start":2750760,"end":2751200,"confidence":0.8933919,"speaker":"A"},{"text":"management","start":2751200,"end":2751480,"confidence":0.99121094,"speaker":"A"},{"text":"that","start":2752600,"end":2752880,"confidence":0.99902344,"speaker":"A"},{"text":"I","start":2752880,"end":2753000,"confidence":0.9658203,"speaker":"A"},{"text":"could.","start":2753000,"end":2753200,"confidence":0.8144531,"speaker":"A"},{"text":"That","start":2753200,"end":2753440,"confidence":0.99902344,"speaker":"A"},{"text":"would","start":2753440,"end":2753560,"confidence":0.9995117,"speaker":"A"},{"text":"be","start":2753560,"end":2753680,"confidence":0.9995117,"speaker":"A"},{"text":"awesome","start":2753680,"end":2754080,"confidence":0.9998372,"speaker":"A"},{"text":"if","start":2754080,"end":2754320,"confidence":0.99902344,"speaker":"A"},{"text":"I","start":2754320,"end":2754440,"confidence":0.9995117,"speaker":"A"},{"text":"could","start":2754440,"end":2754640,"confidence":0.9863281,"speaker":"A"},{"text":"figure","start":2754640,"end":2754920,"confidence":1,"speaker":"A"},{"text":"out","start":2754920,"end":2755040,"confidence":0.9995117,"speaker":"A"},{"text":"how","start":2755040,"end":2755200,"confidence":0.9995117,"speaker":"A"},{"text":"to","start":2755200,"end":2755320,"confidence":1,"speaker":"A"},{"text":"do","start":2755320,"end":2755440,"confidence":0.9995117,"speaker":"A"},{"text":"that.","start":2755440,"end":2755720,"confidence":0.9995117,"speaker":"A"},{"text":"If","start":2755720,"end":2756000,"confidence":0.9995117,"speaker":"A"},{"text":"I","start":2756000,"end":2756120,"confidence":0.9995117,"speaker":"A"},{"text":"could","start":2756120,"end":2756240,"confidence":0.84375,"speaker":"A"},{"text":"figure","start":2756240,"end":2756440,"confidence":1,"speaker":"A"},{"text":"out","start":2756440,"end":2756520,"confidence":0.9995117,"speaker":"A"},{"text":"how","start":2756520,"end":2756600,"confidence":0.99853516,"speaker":"A"},{"text":"to","start":2756600,"end":2756680,"confidence":0.9975586,"speaker":"A"},{"text":"do","start":2756680,"end":2756800,"confidence":0.9921875,"speaker":"A"},{"text":"key","start":2756800,"end":2756960,"confidence":0.9682617,"speaker":"A"},{"text":"path","start":2756960,"end":2757280,"confidence":0.953125,"speaker":"A"},{"text":"query","start":2757280,"end":2757600,"confidence":0.9951172,"speaker":"A"},{"text":"filtering,","start":2757600,"end":2758120,"confidence":0.99934894,"speaker":"A"},{"text":"that","start":2758120,"end":2758320,"confidence":0.99902344,"speaker":"A"},{"text":"would","start":2758320,"end":2758480,"confidence":0.9995117,"speaker":"A"},{"text":"be","start":2758480,"end":2758640,"confidence":0.9995117,"speaker":"A"},{"text":"fantastic.","start":2758640,"end":2759400,"confidence":0.99890137,"speaker":"A"},{"text":"And","start":2761720,"end":2762120,"confidence":0.9951172,"speaker":"A"},{"text":"yeah,","start":2762280,"end":2762760,"confidence":0.9998372,"speaker":"A"},{"text":"but","start":2762760,"end":2762960,"confidence":0.9995117,"speaker":"A"},{"text":"there's","start":2762960,"end":2763200,"confidence":0.87320966,"speaker":"A"},{"text":"a.","start":2763200,"end":2763400,"confidence":0.92626953,"speaker":"A"},{"text":"I","start":2763400,"end":2763560,"confidence":0.9980469,"speaker":"A"},{"text":"mean","start":2763560,"end":2763799,"confidence":0.79785156,"speaker":"A"},{"text":"the","start":2763799,"end":2764120,"confidence":0.9995117,"speaker":"A"},{"text":"basics","start":2764120,"end":2764520,"confidence":0.998291,"speaker":"A"},{"text":"is","start":2764520,"end":2764760,"confidence":0.9941406,"speaker":"A"},{"text":"there","start":2764760,"end":2765040,"confidence":0.9995117,"speaker":"A"},{"text":"as","start":2765040,"end":2765280,"confidence":0.9995117,"speaker":"A"},{"text":"far","start":2765280,"end":2765440,"confidence":1,"speaker":"A"},{"text":"as","start":2765440,"end":2765640,"confidence":0.9995117,"speaker":"A"},{"text":"if","start":2765640,"end":2765840,"confidence":0.9995117,"speaker":"A"},{"text":"you","start":2765840,"end":2765960,"confidence":0.99902344,"speaker":"A"},{"text":"want","start":2765960,"end":2766080,"confidence":0.77685547,"speaker":"A"},{"text":"to","start":2766080,"end":2766240,"confidence":0.9946289,"speaker":"A"},{"text":"do","start":2766240,"end":2766400,"confidence":1,"speaker":"A"},{"text":"anything","start":2766400,"end":2766760,"confidence":0.99975586,"speaker":"A"},{"text":"with","start":2766760,"end":2766960,"confidence":1,"speaker":"A"},{"text":"a","start":2766960,"end":2767120,"confidence":0.99560547,"speaker":"A"},{"text":"record,","start":2767120,"end":2767400,"confidence":0.99902344,"speaker":"A"},{"text":"it's","start":2768040,"end":2768400,"confidence":0.9983724,"speaker":"A"},{"text":"pretty","start":2768400,"end":2768600,"confidence":0.9998372,"speaker":"A"},{"text":"much","start":2768600,"end":2768760,"confidence":0.99853516,"speaker":"A"},{"text":"there.","start":2768760,"end":2769080,"confidence":0.98583984,"speaker":"A"},{"text":"One","start":2769720,"end":2770000,"confidence":0.9848633,"speaker":"A"},{"text":"thing","start":2770000,"end":2770160,"confidence":0.99853516,"speaker":"A"},{"text":"with","start":2770160,"end":2770320,"confidence":0.9995117,"speaker":"A"},{"text":"Celestra","start":2770320,"end":2770880,"confidence":0.7967122,"speaker":"A"},{"text":"is","start":2770880,"end":2771040,"confidence":0.8798828,"speaker":"A"},{"text":"I'd","start":2771040,"end":2771240,"confidence":0.9977214,"speaker":"A"},{"text":"love","start":2771240,"end":2771400,"confidence":0.9995117,"speaker":"A"},{"text":"to","start":2771400,"end":2771560,"confidence":0.9995117,"speaker":"A"},{"text":"be","start":2771560,"end":2771720,"confidence":0.99902344,"speaker":"A"},{"text":"able","start":2771720,"end":2771920,"confidence":0.9995117,"speaker":"A"},{"text":"to","start":2771920,"end":2772080,"confidence":1,"speaker":"A"},{"text":"do","start":2772080,"end":2772280,"confidence":1,"speaker":"A"},{"text":"like","start":2772280,"end":2772560,"confidence":0.99902344,"speaker":"A"},{"text":"test","start":2772560,"end":2772880,"confidence":0.99853516,"speaker":"A"},{"text":"out","start":2772880,"end":2773160,"confidence":0.9970703,"speaker":"A"},{"text":"subscriptions","start":2773160,"end":2773880,"confidence":0.9428711,"speaker":"A"},{"text":"and","start":2774200,"end":2774320,"confidence":0.94921875,"speaker":"A"},{"text":"see","start":2774320,"end":2774480,"confidence":0.9995117,"speaker":"A"},{"text":"how","start":2774480,"end":2774640,"confidence":1,"speaker":"A"},{"text":"that","start":2774640,"end":2774800,"confidence":1,"speaker":"A"},{"text":"works.","start":2774800,"end":2775240,"confidence":1,"speaker":"A"},{"text":"So","start":2775880,"end":2776280,"confidence":0.99609375,"speaker":"A"},{"text":"yeah,","start":2777320,"end":2777840,"confidence":0.9996745,"speaker":"A"},{"text":"that's","start":2777840,"end":2778200,"confidence":1,"speaker":"A"},{"text":"really","start":2778200,"end":2778360,"confidence":0.9995117,"speaker":"A"},{"text":"the","start":2778360,"end":2778560,"confidence":1,"speaker":"A"},{"text":"bulk","start":2778560,"end":2778800,"confidence":0.9817708,"speaker":"A"},{"text":"of","start":2778800,"end":2778960,"confidence":0.9995117,"speaker":"A"},{"text":"my","start":2778960,"end":2779120,"confidence":0.9995117,"speaker":"A"},{"text":"presentation","start":2779120,"end":2779720,"confidence":0.9995117,"speaker":"A"},{"text":"today.","start":2779720,"end":2780040,"confidence":0.99902344,"speaker":"A"},{"text":"Now","start":2781800,"end":2782160,"confidence":0.95751953,"speaker":"A"},{"text":"is.","start":2782160,"end":2782480,"confidence":0.8334961,"speaker":"A"},{"text":"Now","start":2782480,"end":2782720,"confidence":0.99902344,"speaker":"A"},{"text":"it's","start":2782720,"end":2782920,"confidence":0.99869794,"speaker":"A"},{"text":"time","start":2782920,"end":2783040,"confidence":0.9995117,"speaker":"A"},{"text":"to","start":2783040,"end":2783160,"confidence":0.9995117,"speaker":"A"},{"text":"ask","start":2783160,"end":2783280,"confidence":0.99902344,"speaker":"A"},{"text":"me","start":2783280,"end":2783440,"confidence":0.99658203,"speaker":"A"},{"text":"a","start":2783440,"end":2783560,"confidence":0.99902344,"speaker":"A"},{"text":"ton","start":2783560,"end":2783720,"confidence":0.9992676,"speaker":"A"},{"text":"of","start":2783720,"end":2783840,"confidence":0.9995117,"speaker":"A"},{"text":"questions","start":2783840,"end":2784200,"confidence":0.9995117,"speaker":"A"},{"text":"and","start":2784200,"end":2784480,"confidence":0.9814453,"speaker":"A"},{"text":"make","start":2784480,"end":2784720,"confidence":0.9995117,"speaker":"A"},{"text":"me","start":2784720,"end":2784880,"confidence":0.9995117,"speaker":"A"},{"text":"feel","start":2784880,"end":2785040,"confidence":1,"speaker":"A"},{"text":"dumb.","start":2785040,"end":2785480,"confidence":0.98706055,"speaker":"A"},{"text":"Go","start":2785880,"end":2786160,"confidence":0.99121094,"speaker":"A"},{"text":"for","start":2786160,"end":2786320,"confidence":0.9995117,"speaker":"A"},{"text":"it.","start":2786320,"end":2786600,"confidence":0.99853516,"speaker":"A"},{"text":"No,","start":2788440,"end":2788840,"confidence":0.95751953,"speaker":"B"},{"text":"there's","start":2789880,"end":2790319,"confidence":0.9355469,"speaker":"B"},{"text":"a","start":2790319,"end":2790440,"confidence":0.9995117,"speaker":"B"},{"text":"lot","start":2790440,"end":2790600,"confidence":0.9995117,"speaker":"B"},{"text":"there","start":2790600,"end":2790840,"confidence":0.99902344,"speaker":"B"},{"text":"to.","start":2790840,"end":2791160,"confidence":0.98828125,"speaker":"B"},{"text":"To","start":2791400,"end":2791720,"confidence":0.99902344,"speaker":"B"},{"text":"absorb.","start":2791720,"end":2792160,"confidence":0.99938965,"speaker":"B"},{"text":"But","start":2792160,"end":2792320,"confidence":0.9995117,"speaker":"B"},{"text":"I,","start":2792320,"end":2792600,"confidence":0.99121094,"speaker":"B"},{"text":"I","start":2792760,"end":2793120,"confidence":0.99658203,"speaker":"B"},{"text":"like","start":2793120,"end":2793400,"confidence":0.99902344,"speaker":"B"},{"text":"the","start":2793400,"end":2793640,"confidence":0.9995117,"speaker":"B"},{"text":"concept","start":2793640,"end":2794200,"confidence":0.976888,"speaker":"B"},{"text":"and","start":2794440,"end":2794720,"confidence":0.99560547,"speaker":"B"},{"text":"I","start":2794720,"end":2794840,"confidence":0.9995117,"speaker":"B"},{"text":"know","start":2794840,"end":2794960,"confidence":1,"speaker":"B"},{"text":"you've","start":2794960,"end":2795280,"confidence":0.99820966,"speaker":"B"},{"text":"been","start":2795280,"end":2795440,"confidence":0.9995117,"speaker":"B"},{"text":"working","start":2795440,"end":2795640,"confidence":0.9995117,"speaker":"B"},{"text":"on","start":2795640,"end":2795840,"confidence":0.9995117,"speaker":"B"},{"text":"this","start":2795840,"end":2796000,"confidence":0.9995117,"speaker":"B"},{"text":"for","start":2796000,"end":2796120,"confidence":0.9995117,"speaker":"B"},{"text":"a","start":2796120,"end":2796240,"confidence":0.99560547,"speaker":"B"},{"text":"while","start":2796240,"end":2796400,"confidence":1,"speaker":"B"},{"text":"and","start":2796400,"end":2796560,"confidence":0.9458008,"speaker":"B"},{"text":"I","start":2796560,"end":2796680,"confidence":0.9975586,"speaker":"B"},{"text":"always","start":2796680,"end":2796840,"confidence":0.99316406,"speaker":"B"},{"text":"thought","start":2796840,"end":2797040,"confidence":0.99853516,"speaker":"B"},{"text":"it","start":2797040,"end":2797160,"confidence":0.9970703,"speaker":"B"},{"text":"was","start":2797160,"end":2797280,"confidence":0.9951172,"speaker":"B"},{"text":"a","start":2797280,"end":2797440,"confidence":0.9663086,"speaker":"B"},{"text":"pretty","start":2797440,"end":2797640,"confidence":0.99869794,"speaker":"B"},{"text":"cool,","start":2797640,"end":2797960,"confidence":0.9980469,"speaker":"B"},{"text":"pretty","start":2799240,"end":2799560,"confidence":0.9943034,"speaker":"B"},{"text":"cool","start":2799560,"end":2799720,"confidence":0.88549805,"speaker":"B"},{"text":"idea","start":2800030,"end":2800350,"confidence":0.72094727,"speaker":"B"},{"text":"and","start":2800590,"end":2800910,"confidence":0.89404297,"speaker":"B"},{"text":"implementation","start":2800910,"end":2801630,"confidence":0.9941406,"speaker":"B"},{"text":"of","start":2801630,"end":2801910,"confidence":0.9770508,"speaker":"B"},{"text":"this.","start":2801910,"end":2802190,"confidence":0.9897461,"speaker":"B"},{"text":"Questions?","start":2802750,"end":2803470,"confidence":0.9904785,"speaker":"A"},{"text":"So","start":2808990,"end":2809270,"confidence":0.95214844,"speaker":"C"},{"text":"with","start":2809270,"end":2809470,"confidence":0.9628906,"speaker":"C"},{"text":"something","start":2809470,"end":2809710,"confidence":0.9995117,"speaker":"C"},{"text":"like.","start":2809710,"end":2810030,"confidence":0.99853516,"speaker":"C"},{"text":"Accessing","start":2814110,"end":2814750,"confidence":0.78027344,"speaker":"C"},{"text":"CloudKit","start":2814830,"end":2815430,"confidence":0.94202,"speaker":"C"},{"text":"through","start":2815430,"end":2815550,"confidence":0.9946289,"speaker":"C"},{"text":"the","start":2815550,"end":2815709,"confidence":0.99902344,"speaker":"C"},{"text":"web,","start":2815709,"end":2816109,"confidence":0.9916992,"speaker":"C"},{"text":"is","start":2816430,"end":2816830,"confidence":0.9995117,"speaker":"C"},{"text":"this","start":2817150,"end":2817510,"confidence":0.99853516,"speaker":"C"},{"text":"setup","start":2817510,"end":2817910,"confidence":0.95092773,"speaker":"C"},{"text":"more","start":2817910,"end":2818110,"confidence":0.9995117,"speaker":"C"},{"text":"ideal","start":2818110,"end":2818590,"confidence":0.9970703,"speaker":"C"},{"text":"for","start":2818670,"end":2819070,"confidence":0.9995117,"speaker":"C"},{"text":"having","start":2820270,"end":2820630,"confidence":0.9995117,"speaker":"C"},{"text":"your","start":2820630,"end":2820990,"confidence":1,"speaker":"C"},{"text":"server","start":2820990,"end":2821630,"confidence":1,"speaker":"C"},{"text":"do","start":2821870,"end":2822270,"confidence":0.9995117,"speaker":"C"},{"text":"the","start":2822670,"end":2822990,"confidence":0.9980469,"speaker":"C"},{"text":"authentication","start":2822990,"end":2823710,"confidence":1,"speaker":"C"},{"text":"to","start":2823950,"end":2824230,"confidence":0.9970703,"speaker":"C"},{"text":"CloudKit","start":2824230,"end":2824790,"confidence":0.9939,"speaker":"C"},{"text":"with","start":2824790,"end":2824950,"confidence":0.99560547,"speaker":"C"},{"text":"Miskit","start":2824950,"end":2825550,"confidence":0.9923096,"speaker":"C"},{"text":"or","start":2825970,"end":2826210,"confidence":0.9921875,"speaker":"C"},{"text":"is","start":2826290,"end":2826650,"confidence":0.9980469,"speaker":"C"},{"text":"miskit","start":2826650,"end":2827250,"confidence":0.93859863,"speaker":"C"},{"text":"something","start":2827250,"end":2827490,"confidence":0.99853516,"speaker":"C"},{"text":"that","start":2827490,"end":2827650,"confidence":0.99658203,"speaker":"C"},{"text":"you","start":2827650,"end":2827770,"confidence":0.9995117,"speaker":"C"},{"text":"could","start":2827770,"end":2827970,"confidence":0.9970703,"speaker":"C"},{"text":"put","start":2827970,"end":2828210,"confidence":0.9995117,"speaker":"C"},{"text":"into","start":2828210,"end":2828530,"confidence":0.99902344,"speaker":"C"},{"text":"even","start":2828530,"end":2828850,"confidence":0.99560547,"speaker":"C"},{"text":"like","start":2828850,"end":2829050,"confidence":0.9765625,"speaker":"C"},{"text":"a","start":2829050,"end":2829330,"confidence":0.5620117,"speaker":"C"},{"text":"client","start":2829330,"end":2829890,"confidence":0.9987793,"speaker":"C"},{"text":"side,","start":2830130,"end":2830530,"confidence":0.52978516,"speaker":"C"},{"text":"you","start":2832850,"end":2833170,"confidence":0.95751953,"speaker":"C"},{"text":"know,","start":2833170,"end":2833370,"confidence":0.9995117,"speaker":"C"},{"text":"like","start":2833370,"end":2833650,"confidence":0.98583984,"speaker":"C"},{"text":"non","start":2834690,"end":2835090,"confidence":0.99658203,"speaker":"C"},{"text":"Swift","start":2835810,"end":2836290,"confidence":0.99780273,"speaker":"C"},{"text":"application","start":2836290,"end":2836770,"confidence":0.9995117,"speaker":"C"},{"text":"or","start":2836770,"end":2837010,"confidence":0.9140625,"speaker":"C"},{"text":"I","start":2837010,"end":2837210,"confidence":0.6401367,"speaker":"C"},{"text":"guess","start":2837210,"end":2837490,"confidence":0.99975586,"speaker":"C"},{"text":"not","start":2837490,"end":2837730,"confidence":0.9628906,"speaker":"C"},{"text":"non","start":2837730,"end":2837930,"confidence":0.8105469,"speaker":"C"},{"text":"Swift","start":2837930,"end":2838250,"confidence":0.9489746,"speaker":"C"},{"text":"but","start":2838250,"end":2838410,"confidence":0.98876953,"speaker":"C"},{"text":"like","start":2838410,"end":2838610,"confidence":0.98583984,"speaker":"C"},{"text":"non","start":2838610,"end":2838930,"confidence":0.9560547,"speaker":"C"},{"text":"like","start":2839090,"end":2839410,"confidence":0.79785156,"speaker":"C"},{"text":"app","start":2839410,"end":2839690,"confidence":0.99609375,"speaker":"C"},{"text":"application.","start":2839690,"end":2840170,"confidence":0.99853516,"speaker":"C"},{"text":"I'm","start":2840170,"end":2840410,"confidence":0.99397784,"speaker":"C"},{"text":"thinking","start":2840410,"end":2840730,"confidence":0.8215332,"speaker":"C"},{"text":"in","start":2840730,"end":2840970,"confidence":0.6489258,"speaker":"C"},{"text":"the","start":2840970,"end":2841130,"confidence":0.9946289,"speaker":"C"},{"text":"context","start":2841130,"end":2841450,"confidence":0.98502606,"speaker":"C"},{"text":"of","start":2841450,"end":2841570,"confidence":0.99902344,"speaker":"C"},{"text":"like","start":2841570,"end":2841730,"confidence":0.98876953,"speaker":"C"},{"text":"a.","start":2841730,"end":2842049,"confidence":0.71728516,"speaker":"A"},{"text":"I","start":2845730,"end":2845970,"confidence":0.99658203,"speaker":"C"},{"text":"guess","start":2845970,"end":2846170,"confidence":1,"speaker":"C"},{"text":"if","start":2846170,"end":2846290,"confidence":0.9970703,"speaker":"C"},{"text":"I","start":2846290,"end":2846410,"confidence":0.9995117,"speaker":"C"},{"text":"wanted","start":2846410,"end":2846730,"confidence":0.9848633,"speaker":"C"},{"text":"to","start":2846730,"end":2846930,"confidence":1,"speaker":"C"},{"text":"create","start":2846930,"end":2847250,"confidence":0.9995117,"speaker":"C"},{"text":"a","start":2847330,"end":2847730,"confidence":0.87939453,"speaker":"C"},{"text":"something","start":2849970,"end":2850290,"confidence":0.9970703,"speaker":"C"},{"text":"accessing","start":2850290,"end":2850810,"confidence":0.96655273,"speaker":"C"},{"text":"CloudKit","start":2850810,"end":2851330,"confidence":0.99853516,"speaker":"C"},{"text":"that","start":2851330,"end":2851490,"confidence":0.9995117,"speaker":"C"},{"text":"is","start":2851490,"end":2851610,"confidence":0.99902344,"speaker":"C"},{"text":"not","start":2851610,"end":2851810,"confidence":0.9995117,"speaker":"C"},{"text":"your","start":2851810,"end":2852010,"confidence":0.9995117,"speaker":"C"},{"text":"typical","start":2852010,"end":2852370,"confidence":1,"speaker":"C"},{"text":"Mac","start":2852370,"end":2852610,"confidence":0.99780273,"speaker":"C"},{"text":"or","start":2852610,"end":2852730,"confidence":0.9863281,"speaker":"C"},{"text":"iOS","start":2852730,"end":2853090,"confidence":0.9980469,"speaker":"C"},{"text":"app.","start":2853090,"end":2853410,"confidence":0.99853516,"speaker":"C"},{"text":"Can","start":2854880,"end":2855000,"confidence":0.9609375,"speaker":"A"},{"text":"you","start":2855000,"end":2855160,"confidence":0.8486328,"speaker":"A"},{"text":"be","start":2855160,"end":2855400,"confidence":0.9951172,"speaker":"A"},{"text":"more","start":2855400,"end":2855680,"confidence":1,"speaker":"A"},{"text":"specific?","start":2855680,"end":2856160,"confidence":0.99975586,"speaker":"A"},{"text":"I'm","start":2857840,"end":2858200,"confidence":0.99104816,"speaker":"C"},{"text":"looking","start":2858200,"end":2858480,"confidence":0.99902344,"speaker":"C"},{"text":"into","start":2858720,"end":2859120,"confidence":0.99560547,"speaker":"C"},{"text":"one.","start":2859280,"end":2859640,"confidence":0.45483398,"speaker":"C"},{"text":"One","start":2859640,"end":2859880,"confidence":1,"speaker":"C"},{"text":"approach","start":2859880,"end":2860120,"confidence":1,"speaker":"C"},{"text":"would","start":2860120,"end":2860400,"confidence":0.99560547,"speaker":"C"},{"text":"be","start":2860400,"end":2860720,"confidence":0.99853516,"speaker":"C"},{"text":"browser","start":2861600,"end":2862040,"confidence":0.9998372,"speaker":"C"},{"text":"extensions.","start":2862040,"end":2862560,"confidence":0.99869794,"speaker":"C"},{"text":"So","start":2865040,"end":2865440,"confidence":0.67871094,"speaker":"A"},{"text":"for","start":2865680,"end":2866000,"confidence":0.9926758,"speaker":"A"},{"text":"like","start":2866000,"end":2866200,"confidence":0.9321289,"speaker":"A"},{"text":"a","start":2866200,"end":2866320,"confidence":0.99121094,"speaker":"A"},{"text":"non","start":2866320,"end":2866520,"confidence":0.99560547,"speaker":"A"},{"text":"Safari","start":2866520,"end":2867080,"confidence":0.9980469,"speaker":"A"},{"text":"browser.","start":2867080,"end":2867680,"confidence":0.99609375,"speaker":"A"},{"text":"Yes.","start":2867760,"end":2868240,"confidence":0.99121094,"speaker":"C"},{"text":"Yeah,","start":2870400,"end":2870720,"confidence":0.9814453,"speaker":"A"},{"text":"this","start":2870720,"end":2870840,"confidence":0.9975586,"speaker":"A"},{"text":"would","start":2870840,"end":2871000,"confidence":0.9975586,"speaker":"A"},{"text":"be","start":2871000,"end":2871160,"confidence":0.9995117,"speaker":"A"},{"text":"great.","start":2871160,"end":2871400,"confidence":1,"speaker":"A"},{"text":"So","start":2871400,"end":2871600,"confidence":0.96240234,"speaker":"A"},{"text":"basically","start":2871600,"end":2872000,"confidence":0.99902344,"speaker":"A"},{"text":"the","start":2873040,"end":2873320,"confidence":0.9995117,"speaker":"A"},{"text":"way","start":2873320,"end":2873560,"confidence":0.9995117,"speaker":"A"},{"text":"you'd","start":2873560,"end":2873960,"confidence":0.98860675,"speaker":"A"},{"text":"want","start":2873960,"end":2874120,"confidence":1,"speaker":"A"},{"text":"that","start":2874120,"end":2874320,"confidence":0.9995117,"speaker":"A"},{"text":"to","start":2874320,"end":2874560,"confidence":0.99853516,"speaker":"A"},{"text":"work,","start":2874560,"end":2874880,"confidence":0.99902344,"speaker":"A"},{"text":"like","start":2875040,"end":2875400,"confidence":0.73095703,"speaker":"A"},{"text":"the","start":2875400,"end":2875640,"confidence":0.9980469,"speaker":"A"},{"text":"sticky","start":2875640,"end":2876040,"confidence":0.9973958,"speaker":"A"},{"text":"part","start":2876040,"end":2876200,"confidence":0.9995117,"speaker":"A"},{"text":"to","start":2876200,"end":2876360,"confidence":0.9980469,"speaker":"A"},{"text":"me","start":2876360,"end":2876560,"confidence":0.9995117,"speaker":"A"},{"text":"would","start":2876560,"end":2876760,"confidence":0.9980469,"speaker":"A"},{"text":"be","start":2876760,"end":2876920,"confidence":0.9995117,"speaker":"A"},{"text":"getting","start":2876920,"end":2877120,"confidence":0.9980469,"speaker":"A"},{"text":"the","start":2877120,"end":2877320,"confidence":0.99902344,"speaker":"A"},{"text":"web","start":2877320,"end":2877560,"confidence":0.998291,"speaker":"A"},{"text":"authentication","start":2877560,"end":2878240,"confidence":0.92614746,"speaker":"A"},{"text":"token.","start":2878240,"end":2878640,"confidence":0.99934894,"speaker":"A"},{"text":"Other","start":2878640,"end":2878880,"confidence":0.99316406,"speaker":"A"},{"text":"than","start":2878880,"end":2879080,"confidence":0.99560547,"speaker":"A"},{"text":"that,","start":2879080,"end":2879360,"confidence":0.97509766,"speaker":"A"},{"text":"like","start":2879440,"end":2879840,"confidence":0.7050781,"speaker":"A"},{"text":"have","start":2880370,"end":2880530,"confidence":0.9765625,"speaker":"A"},{"text":"at","start":2880530,"end":2880770,"confidence":0.515625,"speaker":"A"},{"text":"it.","start":2880770,"end":2881090,"confidence":0.9980469,"speaker":"A"},{"text":"So","start":2884610,"end":2884890,"confidence":0.97802734,"speaker":"A"},{"text":"I'm","start":2884890,"end":2885050,"confidence":0.98339844,"speaker":"A"},{"text":"gonna,","start":2885050,"end":2885250,"confidence":0.8352051,"speaker":"A"},{"text":"I'm","start":2885250,"end":2885410,"confidence":0.9949544,"speaker":"A"},{"text":"gonna","start":2885410,"end":2885570,"confidence":0.9736328,"speaker":"A"},{"text":"be","start":2885570,"end":2885690,"confidence":0.99853516,"speaker":"A"},{"text":"devil's","start":2885690,"end":2886050,"confidence":0.9608154,"speaker":"A"},{"text":"advocate.","start":2886050,"end":2886610,"confidence":0.9995117,"speaker":"A"},{"text":"Why","start":2886690,"end":2887010,"confidence":0.99609375,"speaker":"A"},{"text":"not","start":2887010,"end":2887290,"confidence":1,"speaker":"A"},{"text":"just","start":2887290,"end":2887570,"confidence":0.9995117,"speaker":"A"},{"text":"use","start":2887570,"end":2887810,"confidence":0.9995117,"speaker":"A"},{"text":"the","start":2887810,"end":2888090,"confidence":0.9995117,"speaker":"A"},{"text":"CloudKit","start":2888090,"end":2888770,"confidence":0.87769,"speaker":"A"},{"text":"JavaScript","start":2888850,"end":2889730,"confidence":0.99454755,"speaker":"A"},{"text":"library.","start":2889730,"end":2890210,"confidence":0.8435872,"speaker":"A"},{"text":"If","start":2890210,"end":2890450,"confidence":0.5620117,"speaker":"C"},{"text":"it's","start":2890450,"end":2890690,"confidence":0.9998372,"speaker":"C"},{"text":"an","start":2890690,"end":2890890,"confidence":0.8232422,"speaker":"C"},{"text":"extension,","start":2890890,"end":2891490,"confidence":0.9998372,"speaker":"C"},{"text":"my","start":2892450,"end":2892770,"confidence":0.99853516,"speaker":"C"},{"text":"brain","start":2892770,"end":2893090,"confidence":1,"speaker":"C"},{"text":"jumps","start":2893090,"end":2893450,"confidence":0.9998372,"speaker":"C"},{"text":"to","start":2893450,"end":2893610,"confidence":0.9995117,"speaker":"C"},{"text":"Swift","start":2893610,"end":2893970,"confidence":0.9914551,"speaker":"C"},{"text":"first.","start":2893970,"end":2894290,"confidence":0.9975586,"speaker":"C"},{"text":"Right.","start":2895730,"end":2896129,"confidence":0.97021484,"speaker":"A"},{"text":"But","start":2896129,"end":2896410,"confidence":0.9995117,"speaker":"A"},{"text":"it's","start":2896410,"end":2896730,"confidence":0.96875,"speaker":"A"},{"text":"the","start":2896730,"end":2896970,"confidence":1,"speaker":"A"},{"text":"reason","start":2896970,"end":2897130,"confidence":0.99902344,"speaker":"A"},{"text":"I'm","start":2897130,"end":2897330,"confidence":0.9954427,"speaker":"A"},{"text":"asking","start":2897330,"end":2897610,"confidence":0.97094727,"speaker":"A"},{"text":"that","start":2897610,"end":2897810,"confidence":0.9765625,"speaker":"A"},{"text":"is","start":2897810,"end":2898090,"confidence":0.9980469,"speaker":"A"},{"text":"like","start":2898090,"end":2898370,"confidence":0.9921875,"speaker":"A"},{"text":"it's","start":2898370,"end":2898690,"confidence":0.9900716,"speaker":"A"},{"text":"a,","start":2898690,"end":2898930,"confidence":0.98291016,"speaker":"A"},{"text":"it's","start":2899410,"end":2899770,"confidence":0.9996745,"speaker":"A"},{"text":"already","start":2899770,"end":2899970,"confidence":0.9995117,"speaker":"A"},{"text":"a","start":2899970,"end":2900130,"confidence":0.9995117,"speaker":"A"},{"text":"web","start":2900130,"end":2900410,"confidence":0.98535156,"speaker":"A"},{"text":"extension.","start":2900410,"end":2900890,"confidence":0.9998372,"speaker":"A"},{"text":"I","start":2900890,"end":2901010,"confidence":0.98535156,"speaker":"A"},{"text":"would","start":2901010,"end":2901130,"confidence":0.98095703,"speaker":"A"},{"text":"assume","start":2901130,"end":2901410,"confidence":0.8614909,"speaker":"A"},{"text":"that","start":2901410,"end":2901570,"confidence":0.5854492,"speaker":"A"},{"text":"is","start":2901570,"end":2901690,"confidence":0.80126953,"speaker":"A"},{"text":"true.","start":2901690,"end":2902050,"confidence":0.9968262,"speaker":"A"},{"text":"That","start":2902690,"end":2903090,"confidence":0.9941406,"speaker":"A"},{"text":"it's","start":2903090,"end":2903490,"confidence":0.98876953,"speaker":"A"},{"text":"90","start":2903490,"end":2903810,"confidence":0.99951,"speaker":"A"},{"text":"web","start":2904290,"end":2904650,"confidence":0.9995117,"speaker":"A"},{"text":"based","start":2904650,"end":2904930,"confidence":0.99902344,"speaker":"A"},{"text":"or","start":2905090,"end":2905410,"confidence":0.99853516,"speaker":"A"},{"text":"JavaScript","start":2905410,"end":2906010,"confidence":0.998291,"speaker":"A"},{"text":"based.","start":2906010,"end":2906290,"confidence":0.99902344,"speaker":"A"},{"text":"So","start":2907120,"end":2907200,"confidence":0.9707031,"speaker":"A"},{"text":"that's","start":2907200,"end":2907360,"confidence":0.99934894,"speaker":"A"},{"text":"where","start":2907360,"end":2907480,"confidence":0.9506836,"speaker":"A"},{"text":"I'm","start":2907480,"end":2907680,"confidence":0.99886066,"speaker":"A"},{"text":"just","start":2907680,"end":2907800,"confidence":0.99560547,"speaker":"A"},{"text":"like,","start":2907800,"end":2908000,"confidence":0.99121094,"speaker":"A"},{"text":"well,","start":2908000,"end":2908320,"confidence":0.9951172,"speaker":"A"},{"text":"you","start":2908320,"end":2908600,"confidence":0.99902344,"speaker":"A"},{"text":"may","start":2908600,"end":2908760,"confidence":0.9995117,"speaker":"A"},{"text":"as","start":2908760,"end":2908920,"confidence":0.9995117,"speaker":"A"},{"text":"well.","start":2908920,"end":2909200,"confidence":0.9995117,"speaker":"A"},{"text":"Like,","start":2909200,"end":2909600,"confidence":0.5307617,"speaker":"A"},{"text":"I","start":2909840,"end":2910120,"confidence":0.77685547,"speaker":"A"},{"text":"would","start":2910120,"end":2910280,"confidence":0.99609375,"speaker":"A"},{"text":"love.","start":2910280,"end":2910560,"confidence":0.99853516,"speaker":"A"},{"text":"I","start":2910640,"end":2910880,"confidence":0.97021484,"speaker":"A"},{"text":"don't","start":2910880,"end":2911000,"confidence":0.9313151,"speaker":"A"},{"text":"want","start":2911000,"end":2911120,"confidence":0.9394531,"speaker":"A"},{"text":"to.","start":2911120,"end":2911320,"confidence":0.94433594,"speaker":"A"},{"text":"Like,","start":2911320,"end":2911560,"confidence":0.81689453,"speaker":"A"},{"text":"I","start":2911560,"end":2911680,"confidence":0.99658203,"speaker":"A"},{"text":"love","start":2911680,"end":2911800,"confidence":0.99365234,"speaker":"A"},{"text":"tooting","start":2911800,"end":2912160,"confidence":0.8005371,"speaker":"A"},{"text":"my","start":2912160,"end":2912320,"confidence":1,"speaker":"A"},{"text":"own","start":2912320,"end":2912480,"confidence":1,"speaker":"A"},{"text":"horn.","start":2912480,"end":2912800,"confidence":0.9995117,"speaker":"A"},{"text":"Right.","start":2912800,"end":2913040,"confidence":0.9838867,"speaker":"A"},{"text":"But","start":2913040,"end":2913280,"confidence":0.9951172,"speaker":"A"},{"text":"like,","start":2913280,"end":2913600,"confidence":0.94628906,"speaker":"A"},{"text":"like","start":2914880,"end":2915280,"confidence":0.82666016,"speaker":"A"},{"text":"why","start":2915280,"end":2915560,"confidence":0.9951172,"speaker":"A"},{"text":"not","start":2915560,"end":2915800,"confidence":0.87939453,"speaker":"A"},{"text":"just.","start":2915800,"end":2916160,"confidence":0.9975586,"speaker":"A"},{"text":"Unless","start":2916320,"end":2916720,"confidence":0.92749023,"speaker":"A"},{"text":"you're.","start":2916720,"end":2917120,"confidence":0.9876302,"speaker":"A"},{"text":"Unless","start":2920720,"end":2921080,"confidence":0.998291,"speaker":"A"},{"text":"you're","start":2921080,"end":2921440,"confidence":0.90478516,"speaker":"A"},{"text":"like","start":2921440,"end":2921840,"confidence":0.94628906,"speaker":"A"},{"text":"building","start":2922000,"end":2922400,"confidence":1,"speaker":"A"},{"text":"a","start":2922480,"end":2922879,"confidence":0.6621094,"speaker":"A"},{"text":"executable,","start":2923040,"end":2923840,"confidence":0.9987793,"speaker":"A"},{"text":"I","start":2924160,"end":2924440,"confidence":0.99316406,"speaker":"A"},{"text":"guess,","start":2924440,"end":2924800,"confidence":1,"speaker":"A"},{"text":"or","start":2924800,"end":2925080,"confidence":0.9970703,"speaker":"A"},{"text":"an","start":2925080,"end":2925240,"confidence":0.9628906,"speaker":"A"},{"text":"app.","start":2925240,"end":2925480,"confidence":0.93652344,"speaker":"A"},{"text":"Ish.","start":2925480,"end":2925920,"confidence":0.7595215,"speaker":"A"},{"text":"And","start":2927760,"end":2928080,"confidence":0.9038086,"speaker":"C"},{"text":"I","start":2928080,"end":2928400,"confidence":0.64697266,"speaker":"C"},{"text":"guess","start":2928400,"end":2928800,"confidence":1,"speaker":"C"},{"text":"another","start":2928800,"end":2929120,"confidence":1,"speaker":"C"},{"text":"application","start":2929120,"end":2929760,"confidence":1,"speaker":"C"},{"text":"for","start":2929760,"end":2930000,"confidence":1,"speaker":"C"},{"text":"this","start":2930000,"end":2930240,"confidence":1,"speaker":"C"},{"text":"would","start":2930240,"end":2930560,"confidence":0.9995117,"speaker":"C"},{"text":"be","start":2930560,"end":2930960,"confidence":0.9995117,"speaker":"C"},{"text":"doing","start":2931680,"end":2932040,"confidence":0.9995117,"speaker":"C"},{"text":"CloudKit","start":2932040,"end":2932680,"confidence":0.99902344,"speaker":"C"},{"text":"stuff","start":2932680,"end":2933000,"confidence":0.9954427,"speaker":"C"},{"text":"server","start":2933000,"end":2933360,"confidence":0.9074707,"speaker":"C"},{"text":"side","start":2933360,"end":2933640,"confidence":1,"speaker":"C"},{"text":"and","start":2933640,"end":2934000,"confidence":0.9243164,"speaker":"C"},{"text":"then","start":2934000,"end":2934400,"confidence":0.9995117,"speaker":"C"},{"text":"providing","start":2934400,"end":2934880,"confidence":0.8515625,"speaker":"C"},{"text":"my","start":2934880,"end":2935120,"confidence":0.9995117,"speaker":"C"},{"text":"own","start":2935120,"end":2935400,"confidence":1,"speaker":"C"},{"text":"API","start":2935400,"end":2935920,"confidence":1,"speaker":"C"},{"text":"layer","start":2935920,"end":2936280,"confidence":0.9995117,"speaker":"C"},{"text":"over","start":2936280,"end":2936480,"confidence":1,"speaker":"C"},{"text":"it.","start":2936480,"end":2936800,"confidence":0.99853516,"speaker":"C"},{"text":"Yep,","start":2937660,"end":2938060,"confidence":0.8959961,"speaker":"A"},{"text":"yep.","start":2938220,"end":2938700,"confidence":0.7453613,"speaker":"A"},{"text":"So","start":2938940,"end":2939340,"confidence":0.9946289,"speaker":"A"},{"text":"that's.","start":2939340,"end":2939860,"confidence":0.9943034,"speaker":"A"},{"text":"Yeah.","start":2939860,"end":2940300,"confidence":0.99316406,"speaker":"A"},{"text":"Are","start":2940460,"end":2940700,"confidence":0.99658203,"speaker":"A"},{"text":"we","start":2940700,"end":2940820,"confidence":0.9995117,"speaker":"A"},{"text":"talking","start":2940820,"end":2941180,"confidence":0.9992676,"speaker":"A"},{"text":"private","start":2941340,"end":2941660,"confidence":0.99902344,"speaker":"A"},{"text":"database","start":2941660,"end":2942180,"confidence":0.9998372,"speaker":"A"},{"text":"or","start":2942180,"end":2942340,"confidence":0.9970703,"speaker":"A"},{"text":"public","start":2942340,"end":2942540,"confidence":0.9995117,"speaker":"A"},{"text":"database?","start":2942540,"end":2943180,"confidence":0.9995117,"speaker":"A"},{"text":"Private.","start":2943340,"end":2943740,"confidence":0.99609375,"speaker":"C"},{"text":"So","start":2945580,"end":2945820,"confidence":0.99902344,"speaker":"A"},{"text":"in","start":2945820,"end":2945940,"confidence":0.9995117,"speaker":"A"},{"text":"that","start":2945940,"end":2946140,"confidence":0.9995117,"speaker":"A"},{"text":"case,","start":2946140,"end":2946460,"confidence":1,"speaker":"A"},{"text":"basically","start":2946700,"end":2947340,"confidence":0.99975586,"speaker":"A"},{"text":"like","start":2948060,"end":2948340,"confidence":0.99853516,"speaker":"A"},{"text":"you'd","start":2948340,"end":2948660,"confidence":0.99690753,"speaker":"A"},{"text":"have","start":2948660,"end":2948780,"confidence":1,"speaker":"A"},{"text":"to","start":2948780,"end":2948900,"confidence":1,"speaker":"A"},{"text":"go","start":2948900,"end":2949140,"confidence":0.9995117,"speaker":"A"},{"text":"the","start":2949140,"end":2949380,"confidence":0.99902344,"speaker":"A"},{"text":"Hard","start":2949380,"end":2949580,"confidence":0.8798828,"speaker":"A"},{"text":"Twitch","start":2949580,"end":2949940,"confidence":0.9433594,"speaker":"A"},{"text":"route","start":2949940,"end":2950300,"confidence":0.9946289,"speaker":"A"},{"text":"and","start":2951100,"end":2951500,"confidence":0.9951172,"speaker":"A"},{"text":"you","start":2952460,"end":2952740,"confidence":0.99853516,"speaker":"A"},{"text":"would","start":2952740,"end":2952979,"confidence":0.8515625,"speaker":"A"},{"text":"have","start":2952979,"end":2953219,"confidence":1,"speaker":"A"},{"text":"to","start":2953219,"end":2953380,"confidence":1,"speaker":"A"},{"text":"provide","start":2953380,"end":2953660,"confidence":1,"speaker":"A"},{"text":"a","start":2953900,"end":2954180,"confidence":0.9760742,"speaker":"A"},{"text":"way","start":2954180,"end":2954460,"confidence":0.9975586,"speaker":"A"},{"text":"to","start":2955980,"end":2956260,"confidence":0.9975586,"speaker":"A"},{"text":"get","start":2956260,"end":2956420,"confidence":1,"speaker":"A"},{"text":"their","start":2956420,"end":2956580,"confidence":0.9921875,"speaker":"A"},{"text":"web","start":2956580,"end":2956820,"confidence":0.9992676,"speaker":"A"},{"text":"authentication","start":2956820,"end":2957420,"confidence":0.9996338,"speaker":"A"},{"text":"token,","start":2957420,"end":2957980,"confidence":0.99820966,"speaker":"A"},{"text":"essentially,","start":2958460,"end":2959060,"confidence":0.9316406,"speaker":"A"},{"text":"if","start":2959060,"end":2959260,"confidence":0.9770508,"speaker":"A"},{"text":"that","start":2959260,"end":2959380,"confidence":0.9995117,"speaker":"A"},{"text":"makes","start":2959380,"end":2959540,"confidence":0.9970703,"speaker":"A"},{"text":"sense.","start":2959540,"end":2959900,"confidence":0.99853516,"speaker":"A"},{"text":"And","start":2960540,"end":2960820,"confidence":0.9975586,"speaker":"A"},{"text":"then","start":2960820,"end":2961020,"confidence":0.99902344,"speaker":"A"},{"text":"store","start":2961020,"end":2961260,"confidence":0.99853516,"speaker":"A"},{"text":"it","start":2961260,"end":2961380,"confidence":0.9980469,"speaker":"A"},{"text":"in","start":2961380,"end":2961540,"confidence":0.9980469,"speaker":"A"},{"text":"Postgres","start":2961540,"end":2962020,"confidence":0.98046875,"speaker":"A"},{"text":"or","start":2962020,"end":2962180,"confidence":0.9970703,"speaker":"A"},{"text":"whatever","start":2962180,"end":2962380,"confidence":0.9995117,"speaker":"A"},{"text":"the","start":2962380,"end":2962500,"confidence":0.99902344,"speaker":"A"},{"text":"hell","start":2962500,"end":2962700,"confidence":0.99975586,"speaker":"A"},{"text":"you","start":2962700,"end":2962820,"confidence":0.9995117,"speaker":"A"},{"text":"want","start":2962820,"end":2962980,"confidence":0.97802734,"speaker":"A"},{"text":"to","start":2962980,"end":2963100,"confidence":0.9980469,"speaker":"A"},{"text":"do.","start":2963100,"end":2963260,"confidence":0.9995117,"speaker":"A"},{"text":"Like","start":2963260,"end":2963500,"confidence":0.99121094,"speaker":"A"},{"text":"that's,","start":2963500,"end":2963820,"confidence":0.98876953,"speaker":"A"},{"text":"that's","start":2963820,"end":2964060,"confidence":0.99658203,"speaker":"A"},{"text":"the","start":2964060,"end":2964140,"confidence":0.99902344,"speaker":"A"},{"text":"way","start":2964140,"end":2964220,"confidence":1,"speaker":"A"},{"text":"I","start":2964220,"end":2964340,"confidence":0.9995117,"speaker":"A"},{"text":"did","start":2964340,"end":2964460,"confidence":0.9941406,"speaker":"A"},{"text":"it","start":2964460,"end":2964540,"confidence":0.9946289,"speaker":"A"},{"text":"with","start":2964540,"end":2964660,"confidence":0.9995117,"speaker":"A"},{"text":"Hard","start":2964660,"end":2964820,"confidence":0.8378906,"speaker":"A"},{"text":"Twitch.","start":2964820,"end":2965260,"confidence":0.88256836,"speaker":"A"},{"text":"But","start":2966400,"end":2966480,"confidence":0.96484375,"speaker":"A"},{"text":"once","start":2966480,"end":2966600,"confidence":0.9897461,"speaker":"A"},{"text":"you","start":2966600,"end":2966760,"confidence":0.9946289,"speaker":"A"},{"text":"have","start":2966760,"end":2966880,"confidence":0.8364258,"speaker":"A"},{"text":"that,","start":2966880,"end":2967120,"confidence":0.5385742,"speaker":"A"},{"text":"you","start":2967120,"end":2967360,"confidence":0.9995117,"speaker":"A"},{"text":"can","start":2967360,"end":2967440,"confidence":0.99902344,"speaker":"A"},{"text":"do","start":2967440,"end":2967520,"confidence":0.9995117,"speaker":"A"},{"text":"anything","start":2967520,"end":2967760,"confidence":0.9995117,"speaker":"A"},{"text":"you","start":2967760,"end":2967880,"confidence":0.9970703,"speaker":"A"},{"text":"want","start":2967880,"end":2968080,"confidence":0.99658203,"speaker":"A"},{"text":"on","start":2968080,"end":2968280,"confidence":0.99853516,"speaker":"A"},{"text":"the","start":2968280,"end":2968440,"confidence":0.99316406,"speaker":"A"},{"text":"server","start":2968440,"end":2968880,"confidence":0.99975586,"speaker":"A"},{"text":"with","start":2969200,"end":2969520,"confidence":0.9980469,"speaker":"A"},{"text":"their","start":2969520,"end":2969840,"confidence":0.98583984,"speaker":"A"},{"text":"private","start":2970240,"end":2970600,"confidence":0.99853516,"speaker":"A"},{"text":"database,","start":2970600,"end":2971200,"confidence":0.9996745,"speaker":"A"},{"text":"if","start":2971200,"end":2971400,"confidence":0.99902344,"speaker":"A"},{"text":"that","start":2971400,"end":2971560,"confidence":0.9995117,"speaker":"A"},{"text":"makes","start":2971560,"end":2971720,"confidence":0.9970703,"speaker":"A"},{"text":"sense.","start":2971720,"end":2972080,"confidence":0.99902344,"speaker":"A"},{"text":"It","start":2972560,"end":2972840,"confidence":0.9692383,"speaker":"C"},{"text":"does.","start":2972840,"end":2973120,"confidence":0.9980469,"speaker":"C"},{"text":"Yep.","start":2973920,"end":2974480,"confidence":0.8156738,"speaker":"A"},{"text":"Yep.","start":2974560,"end":2975120,"confidence":0.7368164,"speaker":"A"},{"text":"A","start":2975920,"end":2976160,"confidence":0.5620117,"speaker":"A"},{"text":"couple","start":2976160,"end":2976360,"confidence":0.99731445,"speaker":"A"},{"text":"of","start":2976360,"end":2976480,"confidence":0.9433594,"speaker":"A"},{"text":"things","start":2976480,"end":2976720,"confidence":0.99902344,"speaker":"A"},{"text":"I","start":2977040,"end":2977320,"confidence":0.9980469,"speaker":"A"},{"text":"wanted","start":2977320,"end":2977560,"confidence":0.9992676,"speaker":"A"},{"text":"to","start":2977560,"end":2977720,"confidence":0.9995117,"speaker":"A"},{"text":"bring","start":2977720,"end":2977920,"confidence":1,"speaker":"A"},{"text":"up,","start":2977920,"end":2978240,"confidence":0.9995117,"speaker":"A"},{"text":"so","start":2978320,"end":2978640,"confidence":0.9765625,"speaker":"A"},{"text":"let's","start":2978640,"end":2978920,"confidence":0.99902344,"speaker":"A"},{"text":"take","start":2978920,"end":2979080,"confidence":1,"speaker":"A"},{"text":"a","start":2979080,"end":2979240,"confidence":1,"speaker":"A"},{"text":"look.","start":2979240,"end":2979520,"confidence":0.9995117,"speaker":"A"},{"text":"So","start":2984000,"end":2984400,"confidence":0.95214844,"speaker":"A"},{"text":"part","start":2986880,"end":2987160,"confidence":0.99902344,"speaker":"A"},{"text":"of","start":2987160,"end":2987280,"confidence":1,"speaker":"A"},{"text":"my","start":2987280,"end":2987400,"confidence":1,"speaker":"A"},{"text":"other","start":2987400,"end":2987640,"confidence":1,"speaker":"A"},{"text":"presentation","start":2987640,"end":2988400,"confidence":1,"speaker":"A"},{"text":"is","start":2988640,"end":2989040,"confidence":0.99853516,"speaker":"A"},{"text":"working,","start":2990000,"end":2990400,"confidence":0.87841797,"speaker":"A"},{"text":"talking","start":2990800,"end":2991160,"confidence":0.7766113,"speaker":"A"},{"text":"about","start":2991160,"end":2991440,"confidence":0.9951172,"speaker":"A"},{"text":"cross","start":2991640,"end":2991880,"confidence":0.998291,"speaker":"A"},{"text":"platform","start":2991880,"end":2992360,"confidence":0.8640137,"speaker":"A"},{"text":"automation","start":2992600,"end":2993320,"confidence":0.9996745,"speaker":"A"},{"text":"type","start":2993640,"end":2994000,"confidence":0.9980469,"speaker":"A"},{"text":"stuff.","start":2994000,"end":2994440,"confidence":1,"speaker":"A"},{"text":"And","start":2995560,"end":2995960,"confidence":0.9868164,"speaker":"A"},{"text":"the","start":2996440,"end":2996760,"confidence":0.9995117,"speaker":"A"},{"text":"one","start":2996760,"end":2997040,"confidence":1,"speaker":"A"},{"text":"issue","start":2997040,"end":2997400,"confidence":0.9995117,"speaker":"A"},{"text":"I've","start":2997400,"end":2997840,"confidence":0.9972331,"speaker":"A"},{"text":"run","start":2997840,"end":2998040,"confidence":0.9995117,"speaker":"A"},{"text":"into","start":2998040,"end":2998360,"confidence":1,"speaker":"A"},{"text":"is.","start":2998440,"end":2998840,"confidence":0.9926758,"speaker":"A"},{"text":"So","start":2998920,"end":2999200,"confidence":0.9921875,"speaker":"A"},{"text":"it","start":2999200,"end":2999360,"confidence":0.9916992,"speaker":"A"},{"text":"basically","start":2999360,"end":2999800,"confidence":0.99975586,"speaker":"A"},{"text":"builds","start":2999800,"end":3000160,"confidence":0.9992676,"speaker":"A"},{"text":"on","start":3000160,"end":3000360,"confidence":0.9995117,"speaker":"A"},{"text":"everything.","start":3000360,"end":3000680,"confidence":1,"speaker":"A"},{"text":"Right","start":3000920,"end":3001240,"confidence":0.9995117,"speaker":"A"},{"text":"now.","start":3001240,"end":3001560,"confidence":0.9995117,"speaker":"A"},{"text":"I'm","start":3007560,"end":3007880,"confidence":0.9977214,"speaker":"A"},{"text":"going","start":3007880,"end":3007960,"confidence":0.6772461,"speaker":"A"},{"text":"to","start":3007960,"end":3008080,"confidence":0.9975586,"speaker":"A"},{"text":"share","start":3008080,"end":3008320,"confidence":0.9995117,"speaker":"A"},{"text":"something.","start":3008320,"end":3008680,"confidence":0.9995117,"speaker":"A"},{"text":"Hey","start":3009880,"end":3010200,"confidence":0.99609375,"speaker":"B"},{"text":"guys,","start":3010200,"end":3010520,"confidence":0.99902344,"speaker":"B"},{"text":"I","start":3011000,"end":3011240,"confidence":0.9770508,"speaker":"B"},{"text":"got","start":3011240,"end":3011320,"confidence":0.99609375,"speaker":"B"},{"text":"to","start":3011320,"end":3011400,"confidence":0.44458008,"speaker":"B"},{"text":"drop.","start":3011400,"end":3011720,"confidence":0.9885254,"speaker":"B"},{"text":"But","start":3011800,"end":3012160,"confidence":0.98291016,"speaker":"B"},{"text":"it","start":3012160,"end":3012400,"confidence":0.9995117,"speaker":"B"},{"text":"was","start":3012400,"end":3012680,"confidence":0.9995117,"speaker":"B"},{"text":"good","start":3012680,"end":3013000,"confidence":0.9995117,"speaker":"B"},{"text":"presentation,","start":3013000,"end":3013480,"confidence":0.9995117,"speaker":"B"},{"text":"Leo.","start":3013480,"end":3014040,"confidence":0.9987793,"speaker":"B"},{"text":"Thank","start":3014040,"end":3014400,"confidence":0.99975586,"speaker":"B"},{"text":"you.","start":3014400,"end":3014680,"confidence":0.9975586,"speaker":"B"},{"text":"Yeah,","start":3014840,"end":3015240,"confidence":0.99088544,"speaker":"A"},{"text":"yeah.","start":3015240,"end":3015560,"confidence":0.9458008,"speaker":"A"},{"text":"If","start":3015560,"end":3015720,"confidence":0.88964844,"speaker":"A"},{"text":"I","start":3015720,"end":3015840,"confidence":0.98876953,"speaker":"A"},{"text":"have","start":3015840,"end":3015960,"confidence":0.9169922,"speaker":"A"},{"text":"more","start":3015960,"end":3016040,"confidence":0.97265625,"speaker":"A"},{"text":"questions,","start":3016040,"end":3016320,"confidence":0.95996094,"speaker":"A"},{"text":"if","start":3016320,"end":3016440,"confidence":0.9589844,"speaker":"A"},{"text":"you","start":3016440,"end":3016520,"confidence":0.9951172,"speaker":"A"},{"text":"have","start":3016520,"end":3016640,"confidence":0.9980469,"speaker":"A"},{"text":"any","start":3016640,"end":3016800,"confidence":0.9995117,"speaker":"A"},{"text":"feedback,","start":3016800,"end":3017160,"confidence":0.9996338,"speaker":"A"},{"text":"just","start":3017160,"end":3017360,"confidence":0.9995117,"speaker":"A"},{"text":"hit","start":3017360,"end":3017520,"confidence":1,"speaker":"A"},{"text":"me","start":3017520,"end":3017640,"confidence":1,"speaker":"A"},{"text":"up","start":3017640,"end":3017760,"confidence":1,"speaker":"A"},{"text":"on","start":3017760,"end":3018040,"confidence":0.99658203,"speaker":"A"},{"text":"Slack.","start":3018950,"end":3019350,"confidence":0.89697266,"speaker":"A"},{"text":"Sounds","start":3019590,"end":3019990,"confidence":0.9978841,"speaker":"B"},{"text":"good.","start":3019990,"end":3020150,"confidence":0.9980469,"speaker":"B"},{"text":"Cool,","start":3020150,"end":3020470,"confidence":0.9345703,"speaker":"A"},{"text":"thank","start":3020470,"end":3020750,"confidence":0.7890625,"speaker":"A"},{"text":"you.","start":3020750,"end":3020950,"confidence":0.99316406,"speaker":"A"},{"text":"Thank","start":3020950,"end":3021230,"confidence":0.94628906,"speaker":"A"},{"text":"you","start":3021230,"end":3021350,"confidence":0.9995117,"speaker":"A"},{"text":"so","start":3021350,"end":3021470,"confidence":0.99853516,"speaker":"A"},{"text":"much","start":3021470,"end":3021590,"confidence":1,"speaker":"A"},{"text":"for","start":3021590,"end":3021710,"confidence":0.9995117,"speaker":"A"},{"text":"helping","start":3021710,"end":3021950,"confidence":0.99975586,"speaker":"A"},{"text":"me","start":3021950,"end":3022150,"confidence":0.81103516,"speaker":"A"},{"text":"set","start":3022150,"end":3022350,"confidence":0.96240234,"speaker":"A"},{"text":"this","start":3022350,"end":3022510,"confidence":0.99365234,"speaker":"A"},{"text":"up.","start":3022510,"end":3022790,"confidence":0.99902344,"speaker":"A"},{"text":"Yeah,","start":3023590,"end":3023990,"confidence":0.95214844,"speaker":"A"},{"text":"talk","start":3023990,"end":3024190,"confidence":0.9824219,"speaker":"A"},{"text":"to","start":3024190,"end":3024350,"confidence":0.99902344,"speaker":"A"},{"text":"you","start":3024350,"end":3024470,"confidence":0.99658203,"speaker":"A"},{"text":"later.","start":3024470,"end":3024710,"confidence":0.9838867,"speaker":"A"},{"text":"Thank","start":3024950,"end":3025230,"confidence":0.9968262,"speaker":"B"},{"text":"you.","start":3025230,"end":3025350,"confidence":0.99902344,"speaker":"B"},{"text":"Bye","start":3025350,"end":3025590,"confidence":0.9824219,"speaker":"B"},{"text":"bye.","start":3025590,"end":3025910,"confidence":0.99316406,"speaker":"B"},{"text":"Yeah,","start":3028870,"end":3029190,"confidence":0.88216144,"speaker":"C"},{"text":"so","start":3029190,"end":3029310,"confidence":0.91308594,"speaker":"C"},{"text":"if","start":3029310,"end":3029430,"confidence":0.99609375,"speaker":"C"},{"text":"you","start":3029430,"end":3029510,"confidence":0.99365234,"speaker":"C"},{"text":"had","start":3029510,"end":3029630,"confidence":0.9638672,"speaker":"C"},{"text":"something","start":3029630,"end":3029830,"confidence":0.9995117,"speaker":"C"},{"text":"else","start":3029830,"end":3030070,"confidence":0.99975586,"speaker":"C"},{"text":"to","start":3030070,"end":3030190,"confidence":0.99853516,"speaker":"C"},{"text":"show,","start":3030190,"end":3030350,"confidence":0.99902344,"speaker":"C"},{"text":"I'm","start":3030350,"end":3030550,"confidence":0.99869794,"speaker":"C"},{"text":"happy","start":3030550,"end":3030750,"confidence":0.9995117,"speaker":"C"},{"text":"to","start":3030750,"end":3030990,"confidence":0.6503906,"speaker":"C"},{"text":"look","start":3030990,"end":3031230,"confidence":0.97021484,"speaker":"C"},{"text":"for.","start":3031230,"end":3031430,"confidence":0.79541016,"speaker":"C"},{"text":"I'm","start":3031430,"end":3031670,"confidence":0.99104816,"speaker":"C"},{"text":"here","start":3031670,"end":3031790,"confidence":0.9995117,"speaker":"C"},{"text":"for","start":3031790,"end":3031910,"confidence":0.9995117,"speaker":"C"},{"text":"a","start":3031910,"end":3031990,"confidence":0.9980469,"speaker":"C"},{"text":"few","start":3031990,"end":3032110,"confidence":0.9995117,"speaker":"C"},{"text":"more","start":3032110,"end":3032270,"confidence":0.9995117,"speaker":"C"},{"text":"minutes","start":3032270,"end":3032510,"confidence":0.9987793,"speaker":"C"},{"text":"as","start":3032510,"end":3032670,"confidence":0.99853516,"speaker":"C"},{"text":"well.","start":3032670,"end":3032950,"confidence":0.99902344,"speaker":"C"},{"text":"Yeah,","start":3033590,"end":3033910,"confidence":0.96402997,"speaker":"A"},{"text":"yeah,","start":3033910,"end":3034070,"confidence":0.90755206,"speaker":"A"},{"text":"yeah.","start":3034070,"end":3034390,"confidence":0.8152669,"speaker":"A"},{"text":"So","start":3038790,"end":3039110,"confidence":0.94628906,"speaker":"A"},{"text":"I","start":3039110,"end":3039350,"confidence":0.9995117,"speaker":"A"},{"text":"have","start":3039350,"end":3039630,"confidence":1,"speaker":"A"},{"text":"the","start":3039630,"end":3039870,"confidence":0.9980469,"speaker":"A"},{"text":"workflow","start":3039870,"end":3040350,"confidence":0.9995117,"speaker":"A"},{"text":"working","start":3040350,"end":3040630,"confidence":0.9995117,"speaker":"A"},{"text":"here","start":3041190,"end":3041590,"confidence":0.99853516,"speaker":"A"},{"text":"and","start":3041670,"end":3041950,"confidence":0.9892578,"speaker":"A"},{"text":"it","start":3041950,"end":3042070,"confidence":0.9995117,"speaker":"A"},{"text":"does","start":3042070,"end":3042270,"confidence":0.99902344,"speaker":"A"},{"text":"Ubuntu,","start":3042270,"end":3043110,"confidence":0.9856445,"speaker":"A"},{"text":"it","start":3044080,"end":3044200,"confidence":0.97216797,"speaker":"A"},{"text":"does","start":3044200,"end":3044400,"confidence":0.99853516,"speaker":"A"},{"text":"Windows,","start":3044400,"end":3044960,"confidence":0.9944661,"speaker":"A"},{"text":"it","start":3045120,"end":3045400,"confidence":0.99365234,"speaker":"A"},{"text":"does","start":3045400,"end":3045600,"confidence":0.98779297,"speaker":"A"},{"text":"Android.","start":3045600,"end":3046120,"confidence":0.9943034,"speaker":"A"},{"text":"So","start":3046120,"end":3046360,"confidence":0.98046875,"speaker":"A"},{"text":"all","start":3046360,"end":3046480,"confidence":0.99853516,"speaker":"A"},{"text":"that","start":3046480,"end":3046600,"confidence":0.9975586,"speaker":"A"},{"text":"stuff","start":3046600,"end":3046880,"confidence":0.90494794,"speaker":"A"},{"text":"is","start":3046880,"end":3047080,"confidence":0.9995117,"speaker":"A"},{"text":"available","start":3047080,"end":3047360,"confidence":0.9995117,"speaker":"A"},{"text":"to","start":3047440,"end":3047720,"confidence":0.99902344,"speaker":"A"},{"text":"you.","start":3047720,"end":3048000,"confidence":0.99853516,"speaker":"A"},{"text":"I","start":3048640,"end":3048960,"confidence":0.9995117,"speaker":"A"},{"text":"would","start":3048960,"end":3049200,"confidence":0.9995117,"speaker":"A"},{"text":"never","start":3049200,"end":3049440,"confidence":1,"speaker":"A"},{"text":"recommend","start":3049440,"end":3049920,"confidence":0.9998372,"speaker":"A"},{"text":"using","start":3049920,"end":3050240,"confidence":0.99902344,"speaker":"A"},{"text":"Miskit","start":3050240,"end":3050920,"confidence":0.9777832,"speaker":"A"},{"text":"on","start":3050920,"end":3051160,"confidence":0.99902344,"speaker":"A"},{"text":"an","start":3051160,"end":3051320,"confidence":0.99902344,"speaker":"A"},{"text":"Apple","start":3051320,"end":3051560,"confidence":1,"speaker":"A"},{"text":"platform","start":3051560,"end":3052040,"confidence":0.9992676,"speaker":"A"},{"text":"for","start":3052040,"end":3052280,"confidence":0.9995117,"speaker":"A"},{"text":"obvious","start":3052280,"end":3052640,"confidence":0.99975586,"speaker":"A"},{"text":"reasons,","start":3052640,"end":3053200,"confidence":0.9995117,"speaker":"A"},{"text":"like","start":3053280,"end":3053600,"confidence":0.9238281,"speaker":"A"},{"text":"what's","start":3053600,"end":3053840,"confidence":0.9995117,"speaker":"A"},{"text":"the","start":3053840,"end":3053960,"confidence":0.9995117,"speaker":"A"},{"text":"point?","start":3053960,"end":3054240,"confidence":0.99902344,"speaker":"A"},{"text":"True.","start":3055600,"end":3056080,"confidence":0.9099121,"speaker":"C"},{"text":"Unless","start":3056080,"end":3056440,"confidence":0.99609375,"speaker":"A"},{"text":"there's","start":3056440,"end":3056720,"confidence":0.9946289,"speaker":"A"},{"text":"something","start":3056720,"end":3056920,"confidence":1,"speaker":"A"},{"text":"special","start":3056920,"end":3057240,"confidence":1,"speaker":"A"},{"text":"that","start":3057240,"end":3057480,"confidence":0.9970703,"speaker":"A"},{"text":"I","start":3057480,"end":3057640,"confidence":0.9995117,"speaker":"A"},{"text":"provide","start":3057640,"end":3057880,"confidence":1,"speaker":"A"},{"text":"that","start":3057880,"end":3058160,"confidence":0.9897461,"speaker":"A"},{"text":"CloudKit","start":3058160,"end":3058760,"confidence":0.89551,"speaker":"A"},{"text":"doesn't","start":3058760,"end":3059040,"confidence":0.96777344,"speaker":"A"},{"text":"like,","start":3059040,"end":3059360,"confidence":0.83496094,"speaker":"A"},{"text":"I","start":3059440,"end":3059680,"confidence":0.99560547,"speaker":"A"},{"text":"don't","start":3059680,"end":3059920,"confidence":0.8590495,"speaker":"A"},{"text":"get","start":3059920,"end":3060039,"confidence":0.9995117,"speaker":"A"},{"text":"it.","start":3060039,"end":3060320,"confidence":0.9980469,"speaker":"A"},{"text":"Right.","start":3060480,"end":3060880,"confidence":0.8925781,"speaker":"C"},{"text":"But","start":3061200,"end":3061600,"confidence":0.9941406,"speaker":"A"},{"text":"we","start":3062560,"end":3062880,"confidence":0.9926758,"speaker":"A"},{"text":"have","start":3062880,"end":3063200,"confidence":0.9995117,"speaker":"A"},{"text":"an","start":3063200,"end":3063520,"confidence":0.9770508,"speaker":"A"},{"text":"issue.","start":3063520,"end":3063840,"confidence":0.9765625,"speaker":"A"},{"text":"So","start":3063920,"end":3064200,"confidence":0.9794922,"speaker":"A"},{"text":"I","start":3064200,"end":3064360,"confidence":0.9995117,"speaker":"A"},{"text":"just","start":3064360,"end":3064560,"confidence":0.99902344,"speaker":"A"},{"text":"started","start":3064560,"end":3064840,"confidence":0.9995117,"speaker":"A"},{"text":"dabbling.","start":3064840,"end":3065440,"confidence":0.91918945,"speaker":"A"},{"text":"I","start":3066000,"end":3066280,"confidence":0.609375,"speaker":"A"},{"text":"haven't","start":3066280,"end":3066520,"confidence":0.9489746,"speaker":"A"},{"text":"really","start":3066520,"end":3066800,"confidence":0.9975586,"speaker":"A"},{"text":"done","start":3066960,"end":3067280,"confidence":1,"speaker":"A"},{"text":"anything","start":3067280,"end":3067640,"confidence":1,"speaker":"A"},{"text":"with","start":3067640,"end":3067840,"confidence":0.9995117,"speaker":"A"},{"text":"wasm,","start":3067840,"end":3068480,"confidence":0.6376953,"speaker":"A"},{"text":"but","start":3069450,"end":3069530,"confidence":0.9980469,"speaker":"A"},{"text":"I","start":3069530,"end":3069650,"confidence":0.9980469,"speaker":"A"},{"text":"did","start":3069650,"end":3069810,"confidence":0.99853516,"speaker":"A"},{"text":"definitely","start":3069810,"end":3070210,"confidence":0.83239746,"speaker":"A"},{"text":"try.","start":3070210,"end":3070570,"confidence":0.99902344,"speaker":"A"},{"text":"Like","start":3070570,"end":3070850,"confidence":0.9980469,"speaker":"A"},{"text":"I","start":3070850,"end":3071010,"confidence":0.99609375,"speaker":"A"},{"text":"added","start":3071010,"end":3071250,"confidence":0.99902344,"speaker":"A"},{"text":"support","start":3071250,"end":3071530,"confidence":0.99853516,"speaker":"A"},{"text":"for","start":3071530,"end":3071730,"confidence":0.99853516,"speaker":"A"},{"text":"WASM","start":3071730,"end":3072250,"confidence":0.5599365,"speaker":"A"},{"text":"in","start":3072250,"end":3072450,"confidence":0.9560547,"speaker":"A"},{"text":"my,","start":3072450,"end":3072730,"confidence":0.9975586,"speaker":"A"},{"text":"in","start":3072730,"end":3073050,"confidence":0.9980469,"speaker":"A"},{"text":"my","start":3073050,"end":3073370,"confidence":1,"speaker":"A"},{"text":"Swift","start":3073690,"end":3074210,"confidence":0.9980469,"speaker":"A"},{"text":"build","start":3074210,"end":3074530,"confidence":0.99609375,"speaker":"A"},{"text":"action.","start":3074530,"end":3074890,"confidence":0.99902344,"speaker":"A"},{"text":"The","start":3077210,"end":3077490,"confidence":0.99121094,"speaker":"A"},{"text":"thing","start":3077490,"end":3077650,"confidence":0.9980469,"speaker":"A"},{"text":"about","start":3077650,"end":3077930,"confidence":0.9995117,"speaker":"A"},{"text":"WASA","start":3077930,"end":3078650,"confidence":0.66918945,"speaker":"A"},{"text":"is","start":3078650,"end":3078850,"confidence":0.9995117,"speaker":"A"},{"text":"it","start":3078850,"end":3079010,"confidence":0.99853516,"speaker":"A"},{"text":"does","start":3079010,"end":3079210,"confidence":0.99853516,"speaker":"A"},{"text":"not","start":3079210,"end":3079410,"confidence":0.99560547,"speaker":"A"},{"text":"provide.","start":3079410,"end":3079690,"confidence":0.99902344,"speaker":"A"},{"text":"It","start":3079770,"end":3080050,"confidence":0.99609375,"speaker":"A"},{"text":"doesn't","start":3080050,"end":3080290,"confidence":0.9978841,"speaker":"A"},{"text":"have","start":3080290,"end":3080410,"confidence":1,"speaker":"A"},{"text":"a","start":3080410,"end":3080530,"confidence":0.99853516,"speaker":"A"},{"text":"transport","start":3080530,"end":3081050,"confidence":0.99853516,"speaker":"A"},{"text":"available.","start":3081130,"end":3081530,"confidence":0.9995117,"speaker":"A"},{"text":"So","start":3082570,"end":3082850,"confidence":0.99853516,"speaker":"A"},{"text":"we","start":3082850,"end":3083050,"confidence":0.99853516,"speaker":"A"},{"text":"talked","start":3083050,"end":3083290,"confidence":0.99975586,"speaker":"A"},{"text":"about","start":3083290,"end":3083490,"confidence":0.9995117,"speaker":"A"},{"text":"transports,","start":3083490,"end":3084410,"confidence":0.9938151,"speaker":"A"},{"text":"I","start":3086010,"end":3086250,"confidence":0.9770508,"speaker":"A"},{"text":"think.","start":3086250,"end":3086490,"confidence":0.9980469,"speaker":"A"},{"text":"Did","start":3086570,"end":3086850,"confidence":0.99853516,"speaker":"A"},{"text":"you","start":3086850,"end":3087010,"confidence":1,"speaker":"A"},{"text":"hear","start":3087010,"end":3087170,"confidence":0.9995117,"speaker":"A"},{"text":"about","start":3087170,"end":3087330,"confidence":0.99902344,"speaker":"A"},{"text":"that","start":3087330,"end":3087530,"confidence":0.9970703,"speaker":"A"},{"text":"part","start":3087530,"end":3087770,"confidence":0.9995117,"speaker":"A"},{"text":"about","start":3087770,"end":3087970,"confidence":0.9995117,"speaker":"A"},{"text":"the","start":3087970,"end":3088090,"confidence":0.9995117,"speaker":"A"},{"text":"Open","start":3088090,"end":3088250,"confidence":0.99902344,"speaker":"A"},{"text":"API","start":3088250,"end":3088770,"confidence":0.7873535,"speaker":"A"},{"text":"generator","start":3088770,"end":3089170,"confidence":0.9995117,"speaker":"A"},{"text":"and","start":3089170,"end":3089330,"confidence":0.95751953,"speaker":"A"},{"text":"transports?","start":3089330,"end":3090090,"confidence":0.8383789,"speaker":"A"},{"text":"I","start":3091370,"end":3091770,"confidence":0.9667969,"speaker":"C"},{"text":"think","start":3091850,"end":3092170,"confidence":0.9995117,"speaker":"C"},{"text":"I","start":3092170,"end":3092370,"confidence":0.9970703,"speaker":"C"},{"text":"was","start":3092370,"end":3092570,"confidence":1,"speaker":"C"},{"text":"coming","start":3092570,"end":3092810,"confidence":0.9995117,"speaker":"C"},{"text":"in","start":3092810,"end":3093010,"confidence":0.9980469,"speaker":"C"},{"text":"at","start":3093010,"end":3093130,"confidence":1,"speaker":"C"},{"text":"that","start":3093130,"end":3093330,"confidence":0.99560547,"speaker":"C"},{"text":"point.","start":3093330,"end":3093690,"confidence":0.9980469,"speaker":"C"},{"text":"Okay.","start":3094410,"end":3094920,"confidence":0.92496747,"speaker":"A"},{"text":"When","start":3095630,"end":3095750,"confidence":0.71191406,"speaker":"A"},{"text":"you","start":3095750,"end":3095910,"confidence":0.93408203,"speaker":"A"},{"text":"create","start":3095910,"end":3096070,"confidence":0.99853516,"speaker":"A"},{"text":"a","start":3096070,"end":3096230,"confidence":0.9951172,"speaker":"A"},{"text":"client,","start":3096230,"end":3096670,"confidence":0.9995117,"speaker":"A"},{"text":"so","start":3097630,"end":3097910,"confidence":0.9794922,"speaker":"A"},{"text":"underneath","start":3097910,"end":3098310,"confidence":0.9996745,"speaker":"A"},{"text":"the","start":3098310,"end":3098470,"confidence":0.9995117,"speaker":"A"},{"text":"client","start":3098470,"end":3098910,"confidence":1,"speaker":"A"},{"text":"you","start":3102350,"end":3102630,"confidence":0.99902344,"speaker":"A"},{"text":"have","start":3102630,"end":3102910,"confidence":1,"speaker":"A"},{"text":"what's","start":3102910,"end":3103230,"confidence":0.99934894,"speaker":"A"},{"text":"called","start":3103230,"end":3103350,"confidence":1,"speaker":"A"},{"text":"a","start":3103350,"end":3103510,"confidence":0.7114258,"speaker":"A"},{"text":"client","start":3103510,"end":3103790,"confidence":0.81811523,"speaker":"A"},{"text":"transport.","start":3103790,"end":3104430,"confidence":0.9987793,"speaker":"A"},{"text":"This","start":3104670,"end":3104950,"confidence":0.8666992,"speaker":"A"},{"text":"is","start":3104950,"end":3105230,"confidence":0.99902344,"speaker":"A"},{"text":"so","start":3105630,"end":3105910,"confidence":0.9921875,"speaker":"A"},{"text":"underneath","start":3105910,"end":3106430,"confidence":0.90999347,"speaker":"A"},{"text":"this","start":3106670,"end":3106990,"confidence":0.99902344,"speaker":"A"},{"text":"client,","start":3106990,"end":3107310,"confidence":0.9941406,"speaker":"A"},{"text":"this","start":3107310,"end":3107510,"confidence":0.99902344,"speaker":"A"},{"text":"is","start":3107510,"end":3107630,"confidence":0.9995117,"speaker":"A"},{"text":"an","start":3107630,"end":3107750,"confidence":0.99902344,"speaker":"A"},{"text":"abstraction","start":3107750,"end":3108350,"confidence":0.99975586,"speaker":"A"},{"text":"layer","start":3108350,"end":3108750,"confidence":0.9995117,"speaker":"A"},{"text":"above.","start":3108750,"end":3109150,"confidence":0.8647461,"speaker":"A"},{"text":"So","start":3109870,"end":3110190,"confidence":0.58496094,"speaker":"A"},{"text":"this","start":3110190,"end":3110390,"confidence":0.9995117,"speaker":"A"},{"text":"is","start":3110390,"end":3110550,"confidence":0.99902344,"speaker":"A"},{"text":"not","start":3110550,"end":3110829,"confidence":0.9921875,"speaker":"A"},{"text":"the","start":3110829,"end":3111109,"confidence":0.9995117,"speaker":"A"},{"text":"right","start":3111109,"end":3111270,"confidence":0.99609375,"speaker":"A"},{"text":"one.","start":3111270,"end":3111550,"confidence":0.98339844,"speaker":"A"},{"text":"Where's","start":3112190,"end":3112630,"confidence":0.98323566,"speaker":"A"},{"text":"the","start":3112630,"end":3112790,"confidence":1,"speaker":"A"},{"text":"public","start":3112790,"end":3113030,"confidence":0.9995117,"speaker":"A"},{"text":"one?","start":3113030,"end":3113390,"confidence":0.9916992,"speaker":"A"},{"text":"But","start":3120680,"end":3120800,"confidence":0.99560547,"speaker":"A"},{"text":"anyway,","start":3120800,"end":3121160,"confidence":0.9995117,"speaker":"A"},{"text":"there","start":3121160,"end":3121400,"confidence":0.9980469,"speaker":"A"},{"text":"is","start":3121400,"end":3121720,"confidence":0.9995117,"speaker":"A"},{"text":"here","start":3125080,"end":3125440,"confidence":0.97509766,"speaker":"A"},{"text":"CloudKit","start":3125440,"end":3126040,"confidence":0.98950195,"speaker":"A"},{"text":"service","start":3126040,"end":3126360,"confidence":0.9975586,"speaker":"A"},{"text":"maybe.","start":3126360,"end":3126920,"confidence":0.9958496,"speaker":"A"},{"text":"Yeah,","start":3129560,"end":3129920,"confidence":0.87158203,"speaker":"A"},{"text":"here","start":3129920,"end":3130080,"confidence":0.99853516,"speaker":"A"},{"text":"we","start":3130080,"end":3130240,"confidence":1,"speaker":"A"},{"text":"go.","start":3130240,"end":3130520,"confidence":1,"speaker":"A"},{"text":"So","start":3131320,"end":3131560,"confidence":0.9921875,"speaker":"A"},{"text":"the","start":3131560,"end":3131640,"confidence":0.9995117,"speaker":"A"},{"text":"CloudKit","start":3131640,"end":3132280,"confidence":0.9147949,"speaker":"A"},{"text":"service","start":3132440,"end":3132840,"confidence":0.99609375,"speaker":"A"},{"text":"has","start":3133320,"end":3133640,"confidence":1,"speaker":"A"},{"text":"a","start":3133640,"end":3133840,"confidence":0.9995117,"speaker":"A"},{"text":"client","start":3133840,"end":3134360,"confidence":0.99975586,"speaker":"A"},{"text":"and","start":3135320,"end":3135640,"confidence":0.984375,"speaker":"A"},{"text":"part","start":3135640,"end":3135840,"confidence":1,"speaker":"A"},{"text":"of","start":3135840,"end":3136000,"confidence":1,"speaker":"A"},{"text":"the","start":3136000,"end":3136160,"confidence":1,"speaker":"A"},{"text":"client","start":3136160,"end":3136600,"confidence":0.99975586,"speaker":"A"},{"text":"is","start":3136920,"end":3137240,"confidence":0.99658203,"speaker":"A"},{"text":"being","start":3137240,"end":3137560,"confidence":0.9995117,"speaker":"A"},{"text":"able","start":3137560,"end":3137960,"confidence":0.9995117,"speaker":"A"},{"text":"to","start":3139960,"end":3140360,"confidence":1,"speaker":"A"},{"text":"say","start":3140440,"end":3140760,"confidence":0.9951172,"speaker":"A"},{"text":"what","start":3140760,"end":3140960,"confidence":0.9975586,"speaker":"A"},{"text":"transport","start":3140960,"end":3141520,"confidence":0.99853516,"speaker":"A"},{"text":"you","start":3141520,"end":3141760,"confidence":0.99609375,"speaker":"A"},{"text":"use","start":3141760,"end":3142040,"confidence":0.9970703,"speaker":"A"},{"text":"in","start":3142360,"end":3142640,"confidence":0.9169922,"speaker":"A"},{"text":"Open","start":3142640,"end":3142840,"confidence":0.9995117,"speaker":"A"},{"text":"API.","start":3142840,"end":3143560,"confidence":0.7491455,"speaker":"A"},{"text":"And","start":3144760,"end":3145160,"confidence":0.9868164,"speaker":"A"},{"text":"there's","start":3148850,"end":3149330,"confidence":0.84521484,"speaker":"A"},{"text":"two","start":3149330,"end":3149650,"confidence":0.99609375,"speaker":"A"},{"text":"transports","start":3149970,"end":3150730,"confidence":0.9951172,"speaker":"A"},{"text":"available","start":3150730,"end":3151010,"confidence":0.9995117,"speaker":"A"},{"text":"right","start":3151010,"end":3151330,"confidence":0.9995117,"speaker":"A"},{"text":"now.","start":3151330,"end":3151650,"confidence":0.9970703,"speaker":"A"},{"text":"One","start":3152770,"end":3153170,"confidence":0.9663086,"speaker":"A"},{"text":"is,","start":3153330,"end":3153730,"confidence":0.9975586,"speaker":"A"},{"text":"one","start":3156850,"end":3157170,"confidence":0.9892578,"speaker":"A"},{"text":"is","start":3157170,"end":3157490,"confidence":0.99853516,"speaker":"A"},{"text":"your","start":3157490,"end":3157810,"confidence":0.99658203,"speaker":"A"},{"text":"regular","start":3157810,"end":3158210,"confidence":1,"speaker":"A"},{"text":"URL","start":3158210,"end":3158770,"confidence":0.9992676,"speaker":"A"},{"text":"session","start":3158770,"end":3159130,"confidence":0.99934894,"speaker":"A"},{"text":"for","start":3159130,"end":3159290,"confidence":0.99853516,"speaker":"A"},{"text":"clients,","start":3159290,"end":3159730,"confidence":0.78100586,"speaker":"A"},{"text":"which.","start":3159890,"end":3160210,"confidence":0.99853516,"speaker":"A"},{"text":"That","start":3160210,"end":3160410,"confidence":0.9916992,"speaker":"A"},{"text":"makes","start":3160410,"end":3160610,"confidence":0.9951172,"speaker":"A"},{"text":"sense.","start":3160610,"end":3160930,"confidence":0.9995117,"speaker":"A"},{"text":"Right.","start":3160930,"end":3161250,"confidence":0.9897461,"speaker":"A"},{"text":"And","start":3161570,"end":3161890,"confidence":0.9921875,"speaker":"A"},{"text":"then","start":3161890,"end":3162089,"confidence":0.9892578,"speaker":"A"},{"text":"there's","start":3162089,"end":3162410,"confidence":0.9840495,"speaker":"A"},{"text":"the","start":3162410,"end":3162570,"confidence":0.9584961,"speaker":"A"},{"text":"Async","start":3162570,"end":3163170,"confidence":0.9949951,"speaker":"A"},{"text":"HTTP","start":3163170,"end":3163810,"confidence":0.9881592,"speaker":"A"},{"text":"client","start":3163810,"end":3164170,"confidence":0.9968262,"speaker":"A"},{"text":"which","start":3164170,"end":3164410,"confidence":0.9995117,"speaker":"A"},{"text":"is","start":3164410,"end":3164690,"confidence":0.9995117,"speaker":"A"},{"text":"typically","start":3164690,"end":3165090,"confidence":0.99975586,"speaker":"A"},{"text":"used","start":3165090,"end":3165410,"confidence":0.99658203,"speaker":"A"},{"text":"like","start":3165570,"end":3165850,"confidence":0.9838867,"speaker":"A"},{"text":"Swift","start":3165850,"end":3166130,"confidence":0.89575195,"speaker":"A"},{"text":"NEO","start":3166130,"end":3166530,"confidence":0.94506836,"speaker":"A"},{"text":"based","start":3166530,"end":3166850,"confidence":0.9980469,"speaker":"A"},{"text":"for","start":3167170,"end":3167490,"confidence":0.99560547,"speaker":"A"},{"text":"servers.","start":3167490,"end":3167970,"confidence":0.90649414,"speaker":"A"},{"text":"The","start":3169330,"end":3169610,"confidence":0.99853516,"speaker":"A"},{"text":"thing","start":3169610,"end":3169770,"confidence":0.9995117,"speaker":"A"},{"text":"is","start":3169770,"end":3169970,"confidence":0.9995117,"speaker":"A"},{"text":"that","start":3169970,"end":3170170,"confidence":0.52441406,"speaker":"A"},{"text":"neither","start":3170170,"end":3170410,"confidence":0.99902344,"speaker":"A"},{"text":"of","start":3170410,"end":3170530,"confidence":0.9916992,"speaker":"A"},{"text":"those","start":3170530,"end":3170770,"confidence":0.9980469,"speaker":"A"},{"text":"are","start":3170930,"end":3171250,"confidence":0.99902344,"speaker":"A"},{"text":"available","start":3171250,"end":3171570,"confidence":0.99365234,"speaker":"A"},{"text":"in","start":3171730,"end":3172130,"confidence":0.9638672,"speaker":"A"},{"text":"wasp.","start":3172610,"end":3173170,"confidence":0.58813477,"speaker":"A"},{"text":"Do","start":3174290,"end":3174530,"confidence":0.6435547,"speaker":"A"},{"text":"you","start":3174530,"end":3174610,"confidence":0.99853516,"speaker":"A"},{"text":"know","start":3174610,"end":3174690,"confidence":0.9995117,"speaker":"A"},{"text":"what","start":3174690,"end":3174810,"confidence":0.9980469,"speaker":"A"},{"text":"WASM","start":3174810,"end":3175210,"confidence":0.78027344,"speaker":"A"},{"text":"is?","start":3175210,"end":3175490,"confidence":0.99853516,"speaker":"A"},{"text":"I","start":3176050,"end":3176290,"confidence":0.99902344,"speaker":"C"},{"text":"have","start":3176290,"end":3176410,"confidence":0.9995117,"speaker":"C"},{"text":"no","start":3176410,"end":3176570,"confidence":1,"speaker":"C"},{"text":"experience","start":3176570,"end":3176850,"confidence":1,"speaker":"C"},{"text":"with","start":3176850,"end":3177130,"confidence":0.9995117,"speaker":"C"},{"text":"it,","start":3177130,"end":3177290,"confidence":0.99853516,"speaker":"C"},{"text":"but","start":3177290,"end":3177450,"confidence":0.8720703,"speaker":"C"},{"text":"yes.","start":3177450,"end":3177810,"confidence":0.9963379,"speaker":"C"},{"text":"Okay.","start":3178850,"end":3179410,"confidence":0.9892578,"speaker":"A"},{"text":"It's.","start":3179490,"end":3179850,"confidence":0.96240234,"speaker":"A"},{"text":"It's","start":3179850,"end":3180290,"confidence":0.98811847,"speaker":"A"},{"text":"the","start":3180290,"end":3180570,"confidence":1,"speaker":"A"},{"text":"web","start":3180570,"end":3180810,"confidence":1,"speaker":"A"},{"text":"browser.","start":3180810,"end":3181210,"confidence":0.99869794,"speaker":"A"},{"text":"Right.","start":3181210,"end":3181490,"confidence":0.99853516,"speaker":"A"},{"text":"So.","start":3181890,"end":3182290,"confidence":0.98876953,"speaker":"A"},{"text":"So","start":3182690,"end":3182970,"confidence":0.9975586,"speaker":"A"},{"text":"you","start":3182970,"end":3183130,"confidence":1,"speaker":"A"},{"text":"really","start":3183130,"end":3183290,"confidence":1,"speaker":"A"},{"text":"can't","start":3183290,"end":3183490,"confidence":0.9998372,"speaker":"A"},{"text":"use","start":3183490,"end":3183690,"confidence":0.9995117,"speaker":"A"},{"text":"Miskit","start":3183690,"end":3184370,"confidence":0.95788574,"speaker":"A"},{"text":"in.","start":3184450,"end":3184850,"confidence":0.921875,"speaker":"A"},{"text":"In","start":3186450,"end":3186730,"confidence":0.99609375,"speaker":"A"},{"text":"the.","start":3186730,"end":3186930,"confidence":0.99609375,"speaker":"A"},{"text":"In","start":3186930,"end":3187170,"confidence":0.99658203,"speaker":"A"},{"text":"WASM","start":3187170,"end":3187690,"confidence":0.7368164,"speaker":"A"},{"text":"yet","start":3187690,"end":3187890,"confidence":0.85009766,"speaker":"A"},{"text":"because","start":3187890,"end":3188090,"confidence":1,"speaker":"A"},{"text":"there","start":3188090,"end":3188250,"confidence":1,"speaker":"A"},{"text":"is","start":3188250,"end":3188450,"confidence":0.9975586,"speaker":"A"},{"text":"no","start":3188450,"end":3188649,"confidence":0.9995117,"speaker":"A"},{"text":"transport.","start":3188649,"end":3189170,"confidence":0.998291,"speaker":"A"},{"text":"Now","start":3189170,"end":3189450,"confidence":0.9995117,"speaker":"A"},{"text":"having","start":3189450,"end":3189650,"confidence":1,"speaker":"A"},{"text":"said","start":3189650,"end":3189890,"confidence":1,"speaker":"A"},{"text":"that,","start":3189890,"end":3190210,"confidence":1,"speaker":"A"},{"text":"why","start":3190530,"end":3190850,"confidence":0.99902344,"speaker":"A"},{"text":"on","start":3190850,"end":3191050,"confidence":0.99902344,"speaker":"A"},{"text":"earth","start":3191050,"end":3191290,"confidence":1,"speaker":"A"},{"text":"would","start":3191290,"end":3191450,"confidence":0.9995117,"speaker":"A"},{"text":"you","start":3191450,"end":3191730,"confidence":0.9995117,"speaker":"A"},{"text":"use.","start":3192050,"end":3192450,"confidence":0.99658203,"speaker":"A"},{"text":"Awesome.","start":3193090,"end":3193810,"confidence":0.7972819,"speaker":"A"},{"text":"Why","start":3194050,"end":3194330,"confidence":0.7753906,"speaker":"A"},{"text":"would","start":3194330,"end":3194450,"confidence":0.9667969,"speaker":"A"},{"text":"you","start":3194450,"end":3194530,"confidence":0.8652344,"speaker":"A"},{"text":"use","start":3194530,"end":3194650,"confidence":0.9169922,"speaker":"A"},{"text":"Miskit","start":3194650,"end":3195130,"confidence":0.9088135,"speaker":"A"},{"text":"in","start":3195130,"end":3195250,"confidence":0.99609375,"speaker":"A"},{"text":"the","start":3195250,"end":3195330,"confidence":0.9995117,"speaker":"A"},{"text":"browser?","start":3195330,"end":3195690,"confidence":1,"speaker":"A"},{"text":"Why","start":3195690,"end":3195930,"confidence":0.9995117,"speaker":"A"},{"text":"not","start":3195930,"end":3196090,"confidence":0.9995117,"speaker":"A"},{"text":"just","start":3196090,"end":3196250,"confidence":0.9995117,"speaker":"A"},{"text":"use","start":3196250,"end":3196450,"confidence":0.9995117,"speaker":"A"},{"text":"CloudKit","start":3196450,"end":3196970,"confidence":0.99780273,"speaker":"A"},{"text":"js?","start":3196970,"end":3197410,"confidence":0.8005371,"speaker":"A"},{"text":"So","start":3198380,"end":3198620,"confidence":0.98828125,"speaker":"A"},{"text":"that's","start":3199660,"end":3200100,"confidence":0.9996745,"speaker":"A"},{"text":"essentially,","start":3200100,"end":3200700,"confidence":0.9996338,"speaker":"A"},{"text":"you","start":3201580,"end":3201820,"confidence":0.765625,"speaker":"A"},{"text":"know,","start":3201820,"end":3202060,"confidence":0.77685547,"speaker":"A"},{"text":"What","start":3209260,"end":3209540,"confidence":0.99902344,"speaker":"A"},{"text":"other","start":3209540,"end":3209780,"confidence":0.9975586,"speaker":"A"},{"text":"questions","start":3209780,"end":3210340,"confidence":0.99975586,"speaker":"A"},{"text":"do","start":3210340,"end":3210500,"confidence":0.9995117,"speaker":"A"},{"text":"you","start":3210500,"end":3210660,"confidence":1,"speaker":"A"},{"text":"have?","start":3210660,"end":3210940,"confidence":1,"speaker":"A"},{"text":"My","start":3215660,"end":3216060,"confidence":0.96240234,"speaker":"C"},{"text":"brain","start":3216300,"end":3216780,"confidence":0.99975586,"speaker":"C"},{"text":"is","start":3216780,"end":3217020,"confidence":0.9995117,"speaker":"C"},{"text":"mushy","start":3217020,"end":3217460,"confidence":0.9998372,"speaker":"C"},{"text":"right","start":3217460,"end":3217620,"confidence":0.9995117,"speaker":"C"},{"text":"now,","start":3217620,"end":3217900,"confidence":1,"speaker":"C"},{"text":"so","start":3217900,"end":3218300,"confidence":0.9770508,"speaker":"C"},{"text":"because","start":3221020,"end":3221340,"confidence":0.9970703,"speaker":"A"},{"text":"of","start":3221340,"end":3221540,"confidence":0.99609375,"speaker":"A"},{"text":"my","start":3221540,"end":3221700,"confidence":0.99853516,"speaker":"A"},{"text":"presentation","start":3221700,"end":3222300,"confidence":0.99975586,"speaker":"A"},{"text":"or","start":3222300,"end":3222540,"confidence":0.9902344,"speaker":"A"},{"text":"because","start":3222540,"end":3222860,"confidence":0.99853516,"speaker":"A"},{"text":"other","start":3223020,"end":3223380,"confidence":0.99902344,"speaker":"A"},{"text":"things,","start":3223380,"end":3223740,"confidence":0.9946289,"speaker":"C"},{"text":"I","start":3224570,"end":3224730,"confidence":0.98876953,"speaker":"C"},{"text":"got","start":3224730,"end":3224930,"confidence":0.9995117,"speaker":"C"},{"text":"two","start":3224930,"end":3225090,"confidence":0.9995117,"speaker":"C"},{"text":"hours","start":3225090,"end":3225290,"confidence":1,"speaker":"C"},{"text":"of","start":3225290,"end":3225450,"confidence":0.9873047,"speaker":"C"},{"text":"sleep.","start":3225450,"end":3225850,"confidence":0.9555664,"speaker":"C"},{"text":"Oh,","start":3226650,"end":3226970,"confidence":0.7734375,"speaker":"A"},{"text":"I'm","start":3226970,"end":3227130,"confidence":0.9970703,"speaker":"A"},{"text":"so","start":3227130,"end":3227290,"confidence":0.99365234,"speaker":"A"},{"text":"sorry.","start":3227290,"end":3227690,"confidence":0.9998372,"speaker":"A"},{"text":"So","start":3228170,"end":3228570,"confidence":0.95214844,"speaker":"C"},{"text":"I'm","start":3229770,"end":3230170,"confidence":0.97526044,"speaker":"C"},{"text":"following","start":3230170,"end":3230450,"confidence":0.99853516,"speaker":"C"},{"text":"as","start":3230450,"end":3230690,"confidence":0.9995117,"speaker":"C"},{"text":"best","start":3230690,"end":3230850,"confidence":0.9980469,"speaker":"C"},{"text":"as","start":3230850,"end":3231010,"confidence":0.9941406,"speaker":"C"},{"text":"I","start":3231010,"end":3231170,"confidence":0.9995117,"speaker":"C"},{"text":"can.","start":3231170,"end":3231450,"confidence":0.99902344,"speaker":"C"},{"text":"Snuggling.","start":3234330,"end":3235050,"confidence":0.87927246,"speaker":"A"},{"text":"Yeah,","start":3237050,"end":3237410,"confidence":0.96761066,"speaker":"A"},{"text":"the","start":3237410,"end":3237570,"confidence":0.99609375,"speaker":"A"},{"text":"intro","start":3237570,"end":3238010,"confidence":0.99975586,"speaker":"A"},{"text":"was","start":3238090,"end":3238410,"confidence":0.99853516,"speaker":"A"},{"text":"basically","start":3238410,"end":3238890,"confidence":0.9995117,"speaker":"A"},{"text":"how","start":3239290,"end":3239610,"confidence":0.99902344,"speaker":"A"},{"text":"I","start":3239610,"end":3239930,"confidence":0.9946289,"speaker":"A"},{"text":"originally","start":3240490,"end":3241010,"confidence":0.9998372,"speaker":"A"},{"text":"built","start":3241010,"end":3241250,"confidence":0.992513,"speaker":"A"},{"text":"it","start":3241250,"end":3241410,"confidence":0.9814453,"speaker":"A"},{"text":"for","start":3241410,"end":3241570,"confidence":0.9995117,"speaker":"A"},{"text":"hard","start":3241570,"end":3241730,"confidence":0.4362793,"speaker":"A"},{"text":"Twitch","start":3241730,"end":3242050,"confidence":0.9111328,"speaker":"A"},{"text":"in","start":3242050,"end":3242210,"confidence":0.99316406,"speaker":"A"},{"text":"2020","start":3242210,"end":3242810,"confidence":0.99854,"speaker":"A"},{"text":"for","start":3243210,"end":3243490,"confidence":0.94628906,"speaker":"A"},{"text":"a","start":3243490,"end":3243650,"confidence":0.7871094,"speaker":"A"},{"text":"private","start":3243650,"end":3243890,"confidence":1,"speaker":"A"},{"text":"database","start":3243890,"end":3244570,"confidence":0.99576825,"speaker":"A"},{"text":"login","start":3244730,"end":3245450,"confidence":0.9367676,"speaker":"A"},{"text":"for","start":3245930,"end":3246210,"confidence":0.99902344,"speaker":"A"},{"text":"the","start":3246210,"end":3246370,"confidence":0.9980469,"speaker":"A"},{"text":"Apple","start":3246370,"end":3246650,"confidence":0.99975586,"speaker":"A"},{"text":"Watch","start":3246650,"end":3246890,"confidence":0.8803711,"speaker":"A"},{"text":"because","start":3246890,"end":3247170,"confidence":0.99853516,"speaker":"A"},{"text":"I","start":3247170,"end":3247290,"confidence":0.9975586,"speaker":"A"},{"text":"don't","start":3247290,"end":3247450,"confidence":0.99658203,"speaker":"A"},{"text":"want","start":3247450,"end":3247530,"confidence":0.8720703,"speaker":"A"},{"text":"to","start":3247530,"end":3247610,"confidence":0.9980469,"speaker":"A"},{"text":"have","start":3247610,"end":3247690,"confidence":0.9995117,"speaker":"A"},{"text":"a","start":3247690,"end":3247810,"confidence":0.99853516,"speaker":"A"},{"text":"login","start":3247810,"end":3248210,"confidence":0.99731445,"speaker":"A"},{"text":"screen.","start":3248210,"end":3248490,"confidence":0.99975586,"speaker":"A"},{"text":"And","start":3248490,"end":3248690,"confidence":0.98583984,"speaker":"A"},{"text":"so","start":3248690,"end":3248810,"confidence":0.99902344,"speaker":"A"},{"text":"basically","start":3248810,"end":3249210,"confidence":0.9995117,"speaker":"A"},{"text":"there's","start":3249210,"end":3249570,"confidence":0.99934894,"speaker":"A"},{"text":"a","start":3249570,"end":3249690,"confidence":0.99853516,"speaker":"A"},{"text":"way","start":3249690,"end":3249810,"confidence":0.99853516,"speaker":"A"},{"text":"in","start":3249810,"end":3249930,"confidence":0.9980469,"speaker":"A"},{"text":"the","start":3249930,"end":3250010,"confidence":0.99902344,"speaker":"A"},{"text":"web","start":3250010,"end":3250170,"confidence":0.9995117,"speaker":"A"},{"text":"browser","start":3250170,"end":3250450,"confidence":1,"speaker":"A"},{"text":"to","start":3250450,"end":3250610,"confidence":0.99902344,"speaker":"A"},{"text":"link","start":3250610,"end":3250810,"confidence":0.99975586,"speaker":"A"},{"text":"your","start":3250810,"end":3250970,"confidence":0.99902344,"speaker":"A"},{"text":"Apple","start":3250970,"end":3251290,"confidence":0.9333496,"speaker":"A"},{"text":"Watch","start":3251290,"end":3251610,"confidence":0.9995117,"speaker":"A"},{"text":"to","start":3251770,"end":3252050,"confidence":0.9975586,"speaker":"A"},{"text":"your","start":3252050,"end":3252210,"confidence":0.99902344,"speaker":"A"},{"text":"account","start":3252210,"end":3252490,"confidence":1,"speaker":"A"},{"text":"and","start":3252490,"end":3252770,"confidence":0.99316406,"speaker":"A"},{"text":"then","start":3252770,"end":3252970,"confidence":0.8930664,"speaker":"A"},{"text":"from","start":3252970,"end":3253130,"confidence":1,"speaker":"A"},{"text":"there","start":3253130,"end":3253290,"confidence":1,"speaker":"A"},{"text":"you","start":3253290,"end":3253450,"confidence":1,"speaker":"A"},{"text":"don't","start":3253450,"end":3253610,"confidence":1,"speaker":"A"},{"text":"need","start":3253610,"end":3253730,"confidence":0.9995117,"speaker":"A"},{"text":"to","start":3253730,"end":3253850,"confidence":0.95947266,"speaker":"A"},{"text":"authenticate","start":3253850,"end":3254370,"confidence":0.99975586,"speaker":"A"},{"text":"anymore.","start":3254370,"end":3254890,"confidence":0.991862,"speaker":"A"},{"text":"Nice.","start":3255280,"end":3255600,"confidence":0.94921875,"speaker":"A"},{"text":"I","start":3255760,"end":3256000,"confidence":0.9970703,"speaker":"A"},{"text":"built","start":3256000,"end":3256280,"confidence":0.8284505,"speaker":"A"},{"text":"that","start":3256280,"end":3256440,"confidence":0.9692383,"speaker":"A"},{"text":"all","start":3256440,"end":3256600,"confidence":0.99609375,"speaker":"A"},{"text":"from","start":3256600,"end":3256800,"confidence":1,"speaker":"A"},{"text":"hand","start":3256800,"end":3257120,"confidence":0.9951172,"speaker":"A"},{"text":"and","start":3258400,"end":3258680,"confidence":0.73095703,"speaker":"A"},{"text":"then","start":3258680,"end":3258960,"confidence":0.9941406,"speaker":"A"},{"text":"in","start":3259200,"end":3259520,"confidence":0.9970703,"speaker":"A"},{"text":"23","start":3259520,"end":3260040,"confidence":0.9939,"speaker":"A"},{"text":"they","start":3260040,"end":3260280,"confidence":0.9995117,"speaker":"A"},{"text":"came","start":3260280,"end":3260440,"confidence":0.9995117,"speaker":"A"},{"text":"out","start":3260440,"end":3260560,"confidence":0.94921875,"speaker":"A"},{"text":"with","start":3260560,"end":3260680,"confidence":0.9995117,"speaker":"A"},{"text":"the","start":3260680,"end":3260800,"confidence":0.93652344,"speaker":"A"},{"text":"Open","start":3260800,"end":3261000,"confidence":0.9995117,"speaker":"A"},{"text":"API","start":3261000,"end":3261520,"confidence":0.9807129,"speaker":"A"},{"text":"generator","start":3261520,"end":3262160,"confidence":0.9995117,"speaker":"A"},{"text":"which","start":3262640,"end":3263000,"confidence":0.99609375,"speaker":"A"},{"text":"was","start":3263000,"end":3263280,"confidence":0.64746094,"speaker":"A"},{"text":"like,","start":3263280,"end":3263480,"confidence":0.97558594,"speaker":"A"},{"text":"oh","start":3263480,"end":3263760,"confidence":0.91674805,"speaker":"A"},{"text":"wait,","start":3263760,"end":3264160,"confidence":0.9980469,"speaker":"A"},{"text":"what","start":3264160,"end":3264440,"confidence":0.99121094,"speaker":"A"},{"text":"if","start":3264440,"end":3264720,"confidence":0.99853516,"speaker":"A"},{"text":"I","start":3264800,"end":3265040,"confidence":0.9980469,"speaker":"A"},{"text":"can","start":3265040,"end":3265160,"confidence":0.99658203,"speaker":"A"},{"text":"create","start":3265160,"end":3265320,"confidence":0.99902344,"speaker":"A"},{"text":"an","start":3265320,"end":3265480,"confidence":0.96777344,"speaker":"A"},{"text":"open","start":3265480,"end":3265720,"confidence":0.9995117,"speaker":"A"},{"text":"API","start":3265720,"end":3266320,"confidence":0.98046875,"speaker":"A"},{"text":"file","start":3266800,"end":3267280,"confidence":0.98046875,"speaker":"A"},{"text":"out","start":3267520,"end":3267840,"confidence":0.99560547,"speaker":"A"},{"text":"of","start":3267840,"end":3268160,"confidence":0.99853516,"speaker":"A"},{"text":"Apple's","start":3268320,"end":3269040,"confidence":0.9937744,"speaker":"A"},{"text":"10","start":3269280,"end":3269600,"confidence":0.99951,"speaker":"A"},{"text":"year","start":3269600,"end":3269800,"confidence":0.9995117,"speaker":"A"},{"text":"old","start":3269800,"end":3270000,"confidence":0.99902344,"speaker":"A"},{"text":"documentation?","start":3270000,"end":3270800,"confidence":0.9923828,"speaker":"A"},{"text":"That'd","start":3273120,"end":3273520,"confidence":0.8873698,"speaker":"A"},{"text":"be","start":3273520,"end":3273640,"confidence":1,"speaker":"A"},{"text":"a","start":3273640,"end":3273760,"confidence":0.99902344,"speaker":"A"},{"text":"lot","start":3273760,"end":3273840,"confidence":1,"speaker":"A"},{"text":"of","start":3273840,"end":3273960,"confidence":0.9975586,"speaker":"A"},{"text":"work,","start":3273960,"end":3274160,"confidence":1,"speaker":"A"},{"text":"but","start":3274160,"end":3274400,"confidence":0.6777344,"speaker":"A"},{"text":"I","start":3274400,"end":3274600,"confidence":0.9995117,"speaker":"A"},{"text":"could","start":3274600,"end":3274760,"confidence":0.98876953,"speaker":"A"},{"text":"do","start":3274760,"end":3274920,"confidence":0.9995117,"speaker":"A"},{"text":"it.","start":3274920,"end":3275200,"confidence":0.9995117,"speaker":"A"},{"text":"And","start":3275520,"end":3275920,"confidence":0.8173828,"speaker":"A"},{"text":"I","start":3276000,"end":3276280,"confidence":0.99902344,"speaker":"A"},{"text":"don't","start":3276280,"end":3276480,"confidence":0.9949544,"speaker":"A"},{"text":"know","start":3276480,"end":3276560,"confidence":0.99902344,"speaker":"A"},{"text":"if","start":3276560,"end":3276640,"confidence":1,"speaker":"A"},{"text":"you","start":3276640,"end":3276760,"confidence":0.9995117,"speaker":"A"},{"text":"heard,","start":3276760,"end":3277120,"confidence":0.99902344,"speaker":"A"},{"text":"but","start":3277600,"end":3278000,"confidence":0.9921875,"speaker":"A"},{"text":"there","start":3278960,"end":3279240,"confidence":0.9995117,"speaker":"A"},{"text":"was","start":3279240,"end":3279400,"confidence":0.9589844,"speaker":"A"},{"text":"this","start":3279400,"end":3279560,"confidence":0.9746094,"speaker":"A"},{"text":"thing","start":3279560,"end":3279720,"confidence":0.9995117,"speaker":"A"},{"text":"that","start":3279720,"end":3279840,"confidence":0.99902344,"speaker":"A"},{"text":"came","start":3279840,"end":3279960,"confidence":0.99853516,"speaker":"A"},{"text":"out","start":3279960,"end":3280240,"confidence":0.9980469,"speaker":"A"},{"text":"a","start":3280240,"end":3280480,"confidence":0.99853516,"speaker":"A"},{"text":"couple","start":3280480,"end":3280720,"confidence":0.9992676,"speaker":"A"},{"text":"years","start":3280720,"end":3280920,"confidence":0.9995117,"speaker":"A"},{"text":"ago","start":3280920,"end":3281200,"confidence":0.9980469,"speaker":"A"},{"text":"called","start":3281780,"end":3282020,"confidence":0.99609375,"speaker":"A"},{"text":"AI","start":3282580,"end":3283220,"confidence":0.95092773,"speaker":"A"},{"text":"and","start":3283940,"end":3284340,"confidence":0.9873047,"speaker":"A"},{"text":"it's","start":3284980,"end":3285340,"confidence":0.9996745,"speaker":"A"},{"text":"really","start":3285340,"end":3285500,"confidence":0.9995117,"speaker":"A"},{"text":"good","start":3285500,"end":3285700,"confidence":0.9995117,"speaker":"A"},{"text":"at","start":3285700,"end":3285900,"confidence":0.98095703,"speaker":"A"},{"text":"creating","start":3285900,"end":3286260,"confidence":0.9995117,"speaker":"A"},{"text":"documentation","start":3286260,"end":3286940,"confidence":0.99990237,"speaker":"A"},{"text":"for","start":3286940,"end":3287180,"confidence":1,"speaker":"A"},{"text":"your","start":3287180,"end":3287340,"confidence":0.9995117,"speaker":"A"},{"text":"code,","start":3287340,"end":3287660,"confidence":0.94222003,"speaker":"A"},{"text":"but","start":3287660,"end":3287900,"confidence":0.9975586,"speaker":"A"},{"text":"it's","start":3287900,"end":3288100,"confidence":0.9998372,"speaker":"A"},{"text":"also","start":3288100,"end":3288260,"confidence":0.9995117,"speaker":"A"},{"text":"really","start":3288260,"end":3288500,"confidence":0.5620117,"speaker":"A"},{"text":"good","start":3288500,"end":3288700,"confidence":0.9995117,"speaker":"A"},{"text":"at","start":3288700,"end":3288860,"confidence":0.9995117,"speaker":"A"},{"text":"creating","start":3288860,"end":3289140,"confidence":0.96777344,"speaker":"A"},{"text":"code","start":3289140,"end":3289420,"confidence":0.9996745,"speaker":"A"},{"text":"for","start":3289420,"end":3289620,"confidence":0.9995117,"speaker":"A"},{"text":"your","start":3289620,"end":3289820,"confidence":0.9995117,"speaker":"A"},{"text":"documentation.","start":3289820,"end":3290500,"confidence":0.99902344,"speaker":"A"},{"text":"And","start":3291300,"end":3291580,"confidence":0.8925781,"speaker":"A"},{"text":"so","start":3291580,"end":3291700,"confidence":0.99902344,"speaker":"A"},{"text":"I","start":3291700,"end":3291820,"confidence":0.9975586,"speaker":"A"},{"text":"was","start":3291820,"end":3292020,"confidence":0.9995117,"speaker":"A"},{"text":"like,","start":3292020,"end":3292340,"confidence":0.99658203,"speaker":"A"},{"text":"oh","start":3292500,"end":3292980,"confidence":0.9580078,"speaker":"A"},{"text":"yeah,","start":3293460,"end":3293940,"confidence":0.9975586,"speaker":"A"},{"text":"this","start":3293940,"end":3294220,"confidence":0.9951172,"speaker":"A"},{"text":"is","start":3294220,"end":3294380,"confidence":0.99853516,"speaker":"A"},{"text":"great.","start":3294380,"end":3294660,"confidence":0.9980469,"speaker":"A"},{"text":"Like","start":3295060,"end":3295460,"confidence":0.9238281,"speaker":"A"},{"text":"I","start":3295460,"end":3295740,"confidence":0.9707031,"speaker":"A"},{"text":"can","start":3295740,"end":3295900,"confidence":0.99658203,"speaker":"A"},{"text":"just,","start":3295900,"end":3296180,"confidence":0.9980469,"speaker":"A"},{"text":"I","start":3296740,"end":3296980,"confidence":0.97753906,"speaker":"A"},{"text":"can","start":3296980,"end":3297140,"confidence":0.7270508,"speaker":"A"},{"text":"just","start":3297140,"end":3297420,"confidence":0.9995117,"speaker":"A"},{"text":"Feed","start":3297420,"end":3297739,"confidence":0.9968262,"speaker":"A"},{"text":"it","start":3297739,"end":3297900,"confidence":0.8671875,"speaker":"A"},{"text":"the","start":3297900,"end":3298060,"confidence":0.99853516,"speaker":"A"},{"text":"documentation","start":3298060,"end":3298740,"confidence":0.99921876,"speaker":"A"},{"text":"and","start":3298980,"end":3299380,"confidence":0.9238281,"speaker":"A"},{"text":"go","start":3301140,"end":3301420,"confidence":0.9970703,"speaker":"A"},{"text":"from","start":3301420,"end":3301620,"confidence":0.9995117,"speaker":"A"},{"text":"there.","start":3301620,"end":3301940,"confidence":0.9995117,"speaker":"A"},{"text":"And,","start":3302020,"end":3302340,"confidence":0.97998047,"speaker":"A"},{"text":"like,","start":3302340,"end":3302660,"confidence":0.9477539,"speaker":"A"},{"text":"basically,","start":3302820,"end":3303300,"confidence":0.99975586,"speaker":"A"},{"text":"I've","start":3303300,"end":3303540,"confidence":0.99072266,"speaker":"A"},{"text":"been","start":3303540,"end":3303660,"confidence":0.9902344,"speaker":"A"},{"text":"going","start":3303660,"end":3303860,"confidence":0.9995117,"speaker":"A"},{"text":"step","start":3303860,"end":3304060,"confidence":0.9995117,"speaker":"A"},{"text":"by","start":3304060,"end":3304260,"confidence":1,"speaker":"A"},{"text":"step","start":3304260,"end":3304580,"confidence":1,"speaker":"A"},{"text":"through.","start":3304740,"end":3305140,"confidence":0.98876953,"speaker":"A"},{"text":"Like","start":3305940,"end":3306260,"confidence":0.9980469,"speaker":"A"},{"text":"I","start":3306260,"end":3306460,"confidence":1,"speaker":"A"},{"text":"said,","start":3306460,"end":3306620,"confidence":1,"speaker":"A"},{"text":"if","start":3306620,"end":3306820,"confidence":0.6225586,"speaker":"A"},{"text":"you","start":3306820,"end":3306980,"confidence":1,"speaker":"A"},{"text":"looked","start":3306980,"end":3307220,"confidence":0.9802246,"speaker":"A"},{"text":"at","start":3307220,"end":3307340,"confidence":0.9995117,"speaker":"A"},{"text":"the","start":3307340,"end":3307620,"confidence":0.94140625,"speaker":"A"},{"text":"miskit","start":3307700,"end":3308500,"confidence":0.876709,"speaker":"A"},{"text":"repo,","start":3308780,"end":3309300,"confidence":0.99072266,"speaker":"A"},{"text":"like,","start":3309300,"end":3309580,"confidence":0.9838867,"speaker":"A"},{"text":"I'm","start":3309580,"end":3309820,"confidence":0.9995117,"speaker":"A"},{"text":"going","start":3309820,"end":3309940,"confidence":0.9995117,"speaker":"A"},{"text":"through","start":3309940,"end":3310140,"confidence":0.9995117,"speaker":"A"},{"text":"step","start":3310140,"end":3310340,"confidence":0.9946289,"speaker":"A"},{"text":"by","start":3310340,"end":3310500,"confidence":0.99902344,"speaker":"A"},{"text":"step","start":3310500,"end":3310660,"confidence":1,"speaker":"A"},{"text":"and","start":3310660,"end":3310820,"confidence":0.93896484,"speaker":"A"},{"text":"adding","start":3310820,"end":3311260,"confidence":0.998291,"speaker":"A"},{"text":"new","start":3311660,"end":3312060,"confidence":0.9995117,"speaker":"A"},{"text":"APIs","start":3312380,"end":3313100,"confidence":0.98168945,"speaker":"A"},{"text":"based","start":3314300,"end":3314620,"confidence":0.9995117,"speaker":"A"},{"text":"on","start":3314620,"end":3314780,"confidence":0.9995117,"speaker":"A"},{"text":"what's","start":3314780,"end":3315020,"confidence":0.9996745,"speaker":"A"},{"text":"available","start":3315020,"end":3315220,"confidence":1,"speaker":"A"},{"text":"in","start":3315220,"end":3315460,"confidence":0.95654297,"speaker":"A"},{"text":"the","start":3315460,"end":3315580,"confidence":0.99902344,"speaker":"A"},{"text":"documentation,","start":3315580,"end":3316300,"confidence":0.99677736,"speaker":"A"},{"text":"piece","start":3316700,"end":3317060,"confidence":0.9938151,"speaker":"A"},{"text":"by","start":3317060,"end":3317220,"confidence":0.9291992,"speaker":"A"},{"text":"piece.","start":3317220,"end":3317500,"confidence":0.99332684,"speaker":"A"},{"text":"And","start":3317500,"end":3317660,"confidence":0.99121094,"speaker":"A"},{"text":"I","start":3317660,"end":3317740,"confidence":0.9995117,"speaker":"A"},{"text":"would","start":3317740,"end":3317820,"confidence":1,"speaker":"A"},{"text":"say","start":3317820,"end":3317940,"confidence":1,"speaker":"A"},{"text":"at","start":3317940,"end":3318060,"confidence":0.9995117,"speaker":"A"},{"text":"this","start":3318060,"end":3318180,"confidence":1,"speaker":"A"},{"text":"point,","start":3318180,"end":3318340,"confidence":0.99902344,"speaker":"A"},{"text":"it's","start":3318340,"end":3318580,"confidence":0.9899089,"speaker":"A"},{"text":"like","start":3318580,"end":3318860,"confidence":0.9975586,"speaker":"A"},{"text":"most","start":3319340,"end":3319660,"confidence":1,"speaker":"A"},{"text":"of","start":3319660,"end":3319820,"confidence":0.99902344,"speaker":"A"},{"text":"the","start":3319820,"end":3320020,"confidence":0.99658203,"speaker":"A"},{"text":"really,","start":3320020,"end":3320380,"confidence":0.99658203,"speaker":"A"},{"text":"like","start":3320620,"end":3320940,"confidence":0.98876953,"speaker":"A"},{"text":"80%","start":3320940,"end":3321500,"confidence":0.96655,"speaker":"A"},{"text":"of","start":3321500,"end":3321780,"confidence":0.7285156,"speaker":"A"},{"text":"that","start":3321780,"end":3321940,"confidence":0.9941406,"speaker":"A"},{"text":"people","start":3321940,"end":3322140,"confidence":1,"speaker":"A"},{"text":"use","start":3322140,"end":3322420,"confidence":0.9995117,"speaker":"A"},{"text":"is","start":3322420,"end":3322660,"confidence":0.98876953,"speaker":"A"},{"text":"there.","start":3322660,"end":3322940,"confidence":0.9951172,"speaker":"A"},{"text":"There's","start":3322940,"end":3323340,"confidence":0.9998372,"speaker":"A"},{"text":"like,","start":3323340,"end":3323500,"confidence":0.99121094,"speaker":"A"},{"text":"stuff","start":3323500,"end":3323780,"confidence":0.9995117,"speaker":"A"},{"text":"like","start":3323780,"end":3323980,"confidence":0.99902344,"speaker":"A"},{"text":"subscriptions","start":3323980,"end":3324619,"confidence":0.99501956,"speaker":"A"},{"text":"and","start":3324619,"end":3324940,"confidence":0.99658203,"speaker":"A"},{"text":"zones","start":3324940,"end":3325300,"confidence":0.95703125,"speaker":"A"},{"text":"that","start":3325300,"end":3325660,"confidence":0.99316406,"speaker":"A"},{"text":"I'm","start":3325980,"end":3326340,"confidence":0.9868164,"speaker":"A"},{"text":"still","start":3326340,"end":3326500,"confidence":0.9975586,"speaker":"A"},{"text":"trying","start":3326500,"end":3326700,"confidence":0.9995117,"speaker":"A"},{"text":"to","start":3326700,"end":3326860,"confidence":0.9995117,"speaker":"A"},{"text":"figure","start":3326860,"end":3327140,"confidence":0.99975586,"speaker":"A"},{"text":"out,","start":3327140,"end":3327420,"confidence":0.99121094,"speaker":"A"},{"text":"but","start":3328460,"end":3328780,"confidence":0.9941406,"speaker":"A"},{"text":"it's.","start":3328780,"end":3329100,"confidence":0.9900716,"speaker":"A"},{"text":"It's","start":3329100,"end":3329340,"confidence":0.98746747,"speaker":"A"},{"text":"pretty","start":3329340,"end":3329540,"confidence":0.9991862,"speaker":"A"},{"text":"close","start":3329540,"end":3329740,"confidence":0.9995117,"speaker":"A"},{"text":"to","start":3329740,"end":3329980,"confidence":0.9975586,"speaker":"A"},{"text":"done","start":3329980,"end":3330260,"confidence":0.95410156,"speaker":"A"},{"text":"at","start":3330260,"end":3330460,"confidence":0.99902344,"speaker":"A"},{"text":"this","start":3330460,"end":3330620,"confidence":0.95751953,"speaker":"A"},{"text":"point.","start":3330620,"end":3330940,"confidence":0.66552734,"speaker":"A"},{"text":"Mm.","start":3331260,"end":3331900,"confidence":0.62402344,"speaker":"B"},{"text":"If","start":3335110,"end":3335230,"confidence":0.56103516,"speaker":"A"},{"text":"you","start":3335230,"end":3335350,"confidence":0.99902344,"speaker":"A"},{"text":"use","start":3335350,"end":3335510,"confidence":0.9975586,"speaker":"A"},{"text":"it.","start":3335510,"end":3335830,"confidence":0.5029297,"speaker":"A"},{"text":"Yeah,","start":3336230,"end":3336550,"confidence":0.9943034,"speaker":"C"},{"text":"it's","start":3336550,"end":3336630,"confidence":0.94905597,"speaker":"C"},{"text":"one","start":3336630,"end":3336750,"confidence":0.9902344,"speaker":"C"},{"text":"of","start":3336750,"end":3336870,"confidence":0.99853516,"speaker":"C"},{"text":"those.","start":3336870,"end":3337110,"confidence":0.9760742,"speaker":"C"},{"text":"Because","start":3337270,"end":3337630,"confidence":0.7348633,"speaker":"A"},{"text":"I.","start":3337630,"end":3337990,"confidence":0.86621094,"speaker":"A"},{"text":"Go","start":3338070,"end":3338350,"confidence":0.9902344,"speaker":"A"},{"text":"ahead.","start":3338350,"end":3338590,"confidence":0.9980469,"speaker":"A"},{"text":"Yeah.","start":3338590,"end":3338950,"confidence":0.99397784,"speaker":"C"},{"text":"I","start":3338950,"end":3339110,"confidence":0.49267578,"speaker":"C"},{"text":"was","start":3339110,"end":3339230,"confidence":0.9189453,"speaker":"C"},{"text":"gonna","start":3339230,"end":3339430,"confidence":0.83776855,"speaker":"C"},{"text":"say","start":3339430,"end":3339510,"confidence":1,"speaker":"C"},{"text":"it's","start":3339510,"end":3339670,"confidence":0.9998372,"speaker":"C"},{"text":"one","start":3339670,"end":3339750,"confidence":1,"speaker":"C"},{"text":"of","start":3339750,"end":3339830,"confidence":0.9995117,"speaker":"C"},{"text":"those","start":3339830,"end":3339950,"confidence":0.9995117,"speaker":"C"},{"text":"projects","start":3339950,"end":3340310,"confidence":0.99975586,"speaker":"C"},{"text":"that","start":3340310,"end":3340430,"confidence":1,"speaker":"C"},{"text":"makes","start":3340430,"end":3340590,"confidence":0.9995117,"speaker":"C"},{"text":"me","start":3340590,"end":3340750,"confidence":0.9995117,"speaker":"C"},{"text":"want","start":3340750,"end":3340910,"confidence":0.9604492,"speaker":"C"},{"text":"to","start":3340910,"end":3341070,"confidence":1,"speaker":"C"},{"text":"set","start":3341070,"end":3341230,"confidence":1,"speaker":"C"},{"text":"up","start":3341230,"end":3341390,"confidence":0.9995117,"speaker":"C"},{"text":"a.","start":3341390,"end":3341670,"confidence":0.96240234,"speaker":"C"},{"text":"Like","start":3342150,"end":3342470,"confidence":0.9941406,"speaker":"C"},{"text":"a","start":3342470,"end":3342750,"confidence":0.99902344,"speaker":"C"},{"text":"vapor","start":3342750,"end":3343310,"confidence":0.98551434,"speaker":"C"},{"text":"server","start":3343310,"end":3343630,"confidence":0.9995117,"speaker":"C"},{"text":"or","start":3343630,"end":3343790,"confidence":0.99853516,"speaker":"C"},{"text":"something","start":3343790,"end":3344030,"confidence":1,"speaker":"C"},{"text":"just","start":3344030,"end":3344270,"confidence":1,"speaker":"C"},{"text":"to","start":3344270,"end":3344390,"confidence":1,"speaker":"C"},{"text":"do","start":3344390,"end":3344510,"confidence":0.9995117,"speaker":"C"},{"text":"some","start":3344510,"end":3344670,"confidence":1,"speaker":"C"},{"text":"Swift","start":3344670,"end":3344990,"confidence":0.99975586,"speaker":"C"},{"text":"on","start":3344990,"end":3345110,"confidence":1,"speaker":"C"},{"text":"the","start":3345110,"end":3345230,"confidence":1,"speaker":"C"},{"text":"server.","start":3345230,"end":3345670,"confidence":0.99975586,"speaker":"C"},{"text":"Yeah.","start":3346630,"end":3347110,"confidence":0.9916992,"speaker":"A"},{"text":"Or","start":3347270,"end":3347590,"confidence":0.92041016,"speaker":"A"},{"text":"just","start":3347590,"end":3347830,"confidence":0.99902344,"speaker":"A"},{"text":"like,","start":3347830,"end":3348150,"confidence":0.99658203,"speaker":"A"},{"text":"I","start":3348870,"end":3349150,"confidence":0.9760742,"speaker":"A"},{"text":"wonder","start":3349150,"end":3349390,"confidence":0.9980469,"speaker":"A"},{"text":"if","start":3349390,"end":3349510,"confidence":0.6303711,"speaker":"A"},{"text":"there's","start":3349510,"end":3349710,"confidence":0.867513,"speaker":"A"},{"text":"like,","start":3349710,"end":3349830,"confidence":0.9819336,"speaker":"A"},{"text":"something","start":3349830,"end":3349990,"confidence":0.99902344,"speaker":"A"},{"text":"you","start":3349990,"end":3350189,"confidence":0.9926758,"speaker":"A"},{"text":"do","start":3350189,"end":3350309,"confidence":0.99853516,"speaker":"A"},{"text":"on","start":3350309,"end":3350430,"confidence":0.9970703,"speaker":"A"},{"text":"a","start":3350430,"end":3350590,"confidence":0.9946289,"speaker":"A"},{"text":"pie,","start":3350590,"end":3350950,"confidence":0.7319336,"speaker":"A"},{"text":"like","start":3351750,"end":3352150,"confidence":0.97265625,"speaker":"A"},{"text":"just","start":3352230,"end":3352470,"confidence":0.99853516,"speaker":"A"},{"text":"hook","start":3352470,"end":3352630,"confidence":0.99902344,"speaker":"A"},{"text":"it","start":3352630,"end":3352750,"confidence":0.99853516,"speaker":"A"},{"text":"up","start":3352750,"end":3352870,"confidence":1,"speaker":"A"},{"text":"to","start":3352870,"end":3352990,"confidence":1,"speaker":"A"},{"text":"a","start":3352990,"end":3353110,"confidence":0.9946289,"speaker":"A"},{"text":"CloudKit","start":3353110,"end":3353550,"confidence":0.9953613,"speaker":"A"},{"text":"database.","start":3353550,"end":3353990,"confidence":1,"speaker":"A"},{"text":"Like,","start":3353990,"end":3354190,"confidence":0.99121094,"speaker":"A"},{"text":"there's","start":3354190,"end":3354430,"confidence":0.9998372,"speaker":"A"},{"text":"a","start":3354430,"end":3354550,"confidence":1,"speaker":"A"},{"text":"lot","start":3354550,"end":3354710,"confidence":1,"speaker":"A"},{"text":"you","start":3354710,"end":3354870,"confidence":1,"speaker":"A"},{"text":"could","start":3354870,"end":3354990,"confidence":0.98828125,"speaker":"A"},{"text":"do","start":3354990,"end":3355150,"confidence":1,"speaker":"A"},{"text":"here","start":3355150,"end":3355350,"confidence":1,"speaker":"A"},{"text":"because","start":3355350,"end":3355550,"confidence":0.8598633,"speaker":"A"},{"text":"all","start":3355550,"end":3355710,"confidence":0.9995117,"speaker":"A"},{"text":"you","start":3355710,"end":3355870,"confidence":1,"speaker":"A"},{"text":"need","start":3355870,"end":3356030,"confidence":0.99902344,"speaker":"A"},{"text":"is","start":3356030,"end":3356310,"confidence":0.97314453,"speaker":"A"},{"text":"decent","start":3356710,"end":3357150,"confidence":0.9091797,"speaker":"A"},{"text":"os.","start":3357150,"end":3357510,"confidence":0.95581055,"speaker":"A"},{"text":"I","start":3358950,"end":3359230,"confidence":0.9995117,"speaker":"A"},{"text":"don't","start":3359230,"end":3359430,"confidence":0.9998372,"speaker":"A"},{"text":"know","start":3359430,"end":3359550,"confidence":0.9995117,"speaker":"A"},{"text":"anything","start":3359550,"end":3359870,"confidence":0.99975586,"speaker":"A"},{"text":"about","start":3359870,"end":3360030,"confidence":0.9995117,"speaker":"A"},{"text":"sharing.","start":3360030,"end":3360430,"confidence":0.9663086,"speaker":"A"},{"text":"I","start":3360430,"end":3360670,"confidence":1,"speaker":"A"},{"text":"haven't","start":3360670,"end":3360870,"confidence":0.9992676,"speaker":"A"},{"text":"done","start":3360870,"end":3360990,"confidence":0.9995117,"speaker":"A"},{"text":"anything","start":3360990,"end":3361310,"confidence":0.99975586,"speaker":"A"},{"text":"with","start":3361310,"end":3361470,"confidence":0.8676758,"speaker":"A"},{"text":"sharing","start":3361470,"end":3361830,"confidence":0.99731445,"speaker":"A"},{"text":"yet,","start":3361830,"end":3362110,"confidence":0.98779297,"speaker":"A"},{"text":"so","start":3362110,"end":3362310,"confidence":0.99902344,"speaker":"A"},{"text":"I","start":3362310,"end":3362430,"confidence":0.9663086,"speaker":"A"},{"text":"still","start":3362430,"end":3362590,"confidence":0.9589844,"speaker":"A"},{"text":"have","start":3362590,"end":3362750,"confidence":0.77441406,"speaker":"A"},{"text":"to","start":3362750,"end":3362870,"confidence":0.9995117,"speaker":"A"},{"text":"do","start":3362870,"end":3362990,"confidence":0.9951172,"speaker":"A"},{"text":"that","start":3362990,"end":3363190,"confidence":1,"speaker":"A"},{"text":"and","start":3363190,"end":3363390,"confidence":0.99853516,"speaker":"A"},{"text":"a","start":3363390,"end":3363510,"confidence":0.9995117,"speaker":"A"},{"text":"few","start":3363510,"end":3363630,"confidence":1,"speaker":"A"},{"text":"other","start":3363630,"end":3363830,"confidence":0.99902344,"speaker":"A"},{"text":"things,","start":3363830,"end":3364070,"confidence":0.9995117,"speaker":"A"},{"text":"but.","start":3364070,"end":3364390,"confidence":0.98876953,"speaker":"A"},{"text":"No,","start":3364940,"end":3365180,"confidence":0.6020508,"speaker":"A"},{"text":"yeah,","start":3365180,"end":3365740,"confidence":0.9869792,"speaker":"A"},{"text":"it's","start":3367740,"end":3368060,"confidence":0.97021484,"speaker":"C"},{"text":"an","start":3368060,"end":3368180,"confidence":0.99609375,"speaker":"C"},{"text":"interesting","start":3368180,"end":3368500,"confidence":0.99975586,"speaker":"C"},{"text":"idea.","start":3368500,"end":3368940,"confidence":0.98706055,"speaker":"C"},{"text":"Thank","start":3369900,"end":3370220,"confidence":0.9868164,"speaker":"A"},{"text":"you.","start":3370220,"end":3370460,"confidence":0.9975586,"speaker":"A"},{"text":"Yeah.","start":3371420,"end":3371900,"confidence":0.88997394,"speaker":"B"},{"text":"Well,","start":3371900,"end":3372100,"confidence":0.9980469,"speaker":"A"},{"text":"thank","start":3372100,"end":3372300,"confidence":1,"speaker":"A"},{"text":"you","start":3372300,"end":3372420,"confidence":0.9995117,"speaker":"A"},{"text":"for","start":3372420,"end":3372580,"confidence":0.99902344,"speaker":"A"},{"text":"joining,","start":3372580,"end":3372860,"confidence":0.96809894,"speaker":"A"},{"text":"Josh.","start":3372860,"end":3373260,"confidence":0.98461914,"speaker":"A"},{"text":"Yeah.","start":3373660,"end":3374060,"confidence":0.81844074,"speaker":"C"},{"text":"Thanks","start":3374060,"end":3374300,"confidence":1,"speaker":"C"},{"text":"for","start":3374300,"end":3374460,"confidence":0.9995117,"speaker":"C"},{"text":"hosting","start":3374460,"end":3374820,"confidence":0.9995117,"speaker":"C"},{"text":"this","start":3374820,"end":3375020,"confidence":0.9707031,"speaker":"C"},{"text":"and","start":3375020,"end":3375340,"confidence":0.99902344,"speaker":"C"},{"text":"sharing","start":3375900,"end":3376340,"confidence":0.9934082,"speaker":"C"},{"text":"this","start":3376340,"end":3376500,"confidence":0.9995117,"speaker":"C"},{"text":"info.","start":3376500,"end":3376820,"confidence":0.9995117,"speaker":"C"},{"text":"It's","start":3376820,"end":3377020,"confidence":0.9941406,"speaker":"C"},{"text":"nice.","start":3377020,"end":3377340,"confidence":1,"speaker":"C"},{"text":"Yeah.","start":3378060,"end":3378540,"confidence":0.9866536,"speaker":"A"},{"text":"If","start":3378620,"end":3378980,"confidence":0.9794922,"speaker":"A"},{"text":"you","start":3378980,"end":3379260,"confidence":0.9995117,"speaker":"A"},{"text":"ever","start":3379260,"end":3379500,"confidence":1,"speaker":"A"},{"text":"run","start":3379500,"end":3379700,"confidence":0.9995117,"speaker":"A"},{"text":"into","start":3379700,"end":3379860,"confidence":1,"speaker":"A"},{"text":"anything,","start":3379860,"end":3380180,"confidence":1,"speaker":"A"},{"text":"let","start":3380180,"end":3380300,"confidence":1,"speaker":"A"},{"text":"me","start":3380300,"end":3380459,"confidence":1,"speaker":"A"},{"text":"know.","start":3380459,"end":3380780,"confidence":0.9995117,"speaker":"A"},{"text":"Will","start":3381420,"end":3381740,"confidence":0.5800781,"speaker":"A"},{"text":"do.","start":3381740,"end":3382060,"confidence":0.99365234,"speaker":"A"},{"text":"All","start":3382940,"end":3383220,"confidence":0.9814453,"speaker":"A"},{"text":"right,","start":3383220,"end":3383500,"confidence":1,"speaker":"A"},{"text":"talk","start":3383660,"end":3383940,"confidence":1,"speaker":"A"},{"text":"to","start":3383940,"end":3384100,"confidence":1,"speaker":"A"},{"text":"you","start":3384100,"end":3384220,"confidence":0.9995117,"speaker":"A"},{"text":"later.","start":3384220,"end":3384420,"confidence":1,"speaker":"A"},{"text":"All","start":3384420,"end":3384620,"confidence":0.9223633,"speaker":"A"},{"text":"right,","start":3384620,"end":3384780,"confidence":0.9145508,"speaker":"A"},{"text":"sounds","start":3384780,"end":3385020,"confidence":1,"speaker":"A"},{"text":"good.","start":3385020,"end":3385180,"confidence":1,"speaker":"A"},{"text":"See","start":3385180,"end":3385380,"confidence":0.9975586,"speaker":"C"},{"text":"you.","start":3385380,"end":3385660,"confidence":0.54296875,"speaker":"C"},{"text":"Bye.","start":3386220,"end":3386700,"confidence":0.9375,"speaker":"A"},{"text":"Bye.","start":3386860,"end":3387340,"confidence":0.9519043,"speaker":"C"}] \ No newline at end of file diff --git a/docs/transcriptions/transcript.srt b/docs/transcriptions/transcript.srt new file mode 100644 index 00000000..77d702ca --- /dev/null +++ b/docs/transcriptions/transcript.srt @@ -0,0 +1,2708 @@ +1 +00:04:22,980 --> 00:04:25,700 +Hey, Evan, can you hear me all right? Yeah, I can hear you. + +2 +00:04:26,420 --> 00:04:28,740 +Awesome. How do I sound? Good. + +3 +00:04:30,260 --> 00:04:33,780 +I've used this microphone in ages. It's like all + +4 +00:04:34,280 --> 00:04:34,420 +dusty. + +5 +00:04:41,140 --> 00:04:44,100 +How you think I should wait like five minutes for people to come in or. + +6 +00:04:44,260 --> 00:04:47,530 +Probably. Yeah, that there's if. Yeah, + +7 +00:04:48,010 --> 00:04:51,610 +otherwise you can just. You could start, but that'll be + +8 +00:04:52,110 --> 00:04:54,250 +interesting. Do you mind if I grab a cup of coffee real quick? No, + +9 +00:04:54,750 --> 00:04:58,610 +not at all. Not at all. Okay, cool. I'm not using the AirPods + +10 +00:04:59,110 --> 00:05:01,370 +mic, so I can hear you, but you won't be able to hear me. + +11 +00:05:01,690 --> 00:05:02,250 +Okay. + +12 +00:06:02,440 --> 00:06:27,820 +It's. + +13 +00:08:51,699 --> 00:08:55,060 +Thank you for your patience. + +14 +00:09:09,010 --> 00:09:12,570 +So is it just you? It looks like it's just me. + +15 +00:09:13,070 --> 00:09:16,530 +Josh is trying to get in, but he's trying to get on on his mobile + +16 +00:09:17,030 --> 00:09:19,250 +device and I don't think that's possible with Riverside. + +17 +00:09:23,250 --> 00:09:26,130 +Surprised? I mean, I know they have an app. + +18 +00:09:27,590 --> 00:09:30,070 +Maybe he's using. I'm not sure if he's using. Using the app or not. + +19 +00:09:35,190 --> 00:09:38,630 +Should I just go? Sure. + +20 +00:09:39,830 --> 00:09:43,830 +Okay. Well, thanks for joining me, + +21 +00:09:44,330 --> 00:09:47,790 +Evan. I really appreciate it. I would + +22 +00:09:48,290 --> 00:09:49,910 +say no. I mean I do, seriously. + +23 +00:09:51,830 --> 00:09:55,070 +So yeah, this is a kind of a dry run. I would say + +24 +00:09:55,570 --> 00:09:59,670 +I'm about 60% done with this presentation about + +25 +00:10:00,310 --> 00:10:04,470 +CloudKit on the server and + +26 +00:10:04,870 --> 00:10:08,310 +we'll probably hop back and forth between Keynote and not Keynote, + +27 +00:10:08,870 --> 00:10:12,310 +but yeah. So this is + +28 +00:10:12,810 --> 00:10:16,630 +CloudKit as your backend from iOS to server side Swift. + +29 +00:10:27,600 --> 00:10:31,200 +So what is CloudKit? CloudKit is a service + +30 +00:10:32,240 --> 00:10:36,279 +launched by Apple probably a decade ago to + +31 +00:10:36,779 --> 00:10:40,520 +kind of give developers a built + +32 +00:10:41,020 --> 00:10:43,680 +in back end for storing data for their apps. + +33 +00:10:44,480 --> 00:10:48,250 +One of the biggest benefits is is how cheap it is to + +34 +00:10:48,750 --> 00:10:49,970 +use for iOS developers. + +35 +00:10:52,450 --> 00:10:55,850 +So if you have built an + +36 +00:10:56,350 --> 00:11:01,730 +app, you could just add CloudKit right here within the + +37 +00:11:02,209 --> 00:11:05,970 +Xcode project and use the + +38 +00:11:06,470 --> 00:11:10,130 +regular CloudKit API in Swift to go ahead and start using it + +39 +00:11:10,630 --> 00:11:14,430 +in your app. Here is what + +40 +00:11:14,930 --> 00:11:18,270 +it looks like to create a new record type. You can do all this through + +41 +00:11:18,430 --> 00:11:20,190 +the CloudKit dashboard. + +42 +00:11:24,190 --> 00:11:27,910 +In CloudKit you could also do this using a schema + +43 +00:11:28,410 --> 00:11:32,030 +file too. And you can export and import your schema that + +44 +00:11:32,530 --> 00:11:36,030 +way. And it's not a SQL based database, + +45 +00:11:36,530 --> 00:11:39,910 +it's much more, no sequel ish or an abstract layer + +46 +00:11:40,410 --> 00:11:44,120 +above it. But essentially you can create records + +47 +00:11:44,520 --> 00:11:48,200 +kind of like a table but not quite in your records. + +48 +00:11:49,400 --> 00:11:52,680 +You can create a struct for it. + +49 +00:11:53,180 --> 00:11:56,760 +You can just use CloudKit directly to go ahead and + +50 +00:11:57,260 --> 00:12:00,520 +then you can then plug it into your app and do fun stuff like this. + +51 +00:12:01,560 --> 00:12:05,280 +We can do things like queries and basic + +52 +00:12:05,780 --> 00:12:08,040 +database stuff. There's a lot of advantages to it. + +53 +00:12:09,280 --> 00:12:12,640 +For one, if you're doing Apple only, + +54 +00:12:13,600 --> 00:12:17,040 +then it definitely makes sense to look into, at least look + +55 +00:12:17,540 --> 00:12:18,080 +into CloudKit. + +56 +00:12:22,320 --> 00:12:25,440 +If you're just going to deploy to Apple Devices. + +57 +00:12:26,080 --> 00:12:28,720 +If you don't mind the, + +58 +00:12:29,920 --> 00:12:32,640 +the fact that it's not a regular SQL database, + +59 +00:12:34,050 --> 00:12:37,050 +that's something too to think about. If you like need a SQL database, this might + +60 +00:12:37,550 --> 00:12:41,010 +not be what you want. And then if you don't mind working with + +61 +00:12:41,510 --> 00:12:44,610 +a lot of the abstraction layers that CloudKit provides, + +62 +00:12:46,930 --> 00:12:50,730 +then this might be good for you to get started or especially + +63 +00:12:51,230 --> 00:12:54,930 +if you don't have any database experience. So as far as + +64 +00:12:55,430 --> 00:12:58,690 +like server choices, I would say CloudKit might not be your + +65 +00:12:59,190 --> 00:13:02,890 +first choice, but it certainly is a decent choice if you're + +66 +00:13:03,390 --> 00:13:04,450 +going the Apple only route. + +67 +00:13:09,970 --> 00:13:13,730 +But then the question comes in, why would you want Cloud server side CloudKit? + +68 +00:13:13,890 --> 00:13:16,610 +Why would you want to do anything with CloudKit on the server? + +69 +00:13:17,970 --> 00:13:21,690 +So here's, here's the first case. Well, this is + +70 +00:13:22,190 --> 00:13:26,090 +how you can go ahead and do that is they provide actually a REST API + +71 +00:13:26,590 --> 00:13:30,350 +for calls to CloudKit using the, if you + +72 +00:13:30,850 --> 00:13:35,710 +go to the documentation, I'll provide a link to that CloudKit Web Services which + +73 +00:13:36,510 --> 00:13:39,550 +provides a lot of the documentation for what we'll be talking about today. + +74 +00:13:40,910 --> 00:13:43,790 +A lot of this is abstracted out in the JavaScript library. + +75 +00:13:43,870 --> 00:13:47,150 +So if you want to do stuff on a website, they provide + +76 +00:13:47,230 --> 00:13:51,110 +a CloudKit JavaScript library for + +77 +00:13:51,610 --> 00:13:53,710 +that. Sorry, + +78 +00:13:56,190 --> 00:13:59,230 +just going into do not disturb mode. + +79 +00:14:07,950 --> 00:14:11,070 +They even in that web references documentation + +80 +00:14:11,570 --> 00:14:15,310 +they provide a composing web service request and all these instructions about how to go + +81 +00:14:15,810 --> 00:14:19,110 +ahead and do that. So man, was it like + +82 +00:14:19,610 --> 00:14:23,320 +half a decade ago that I built + +83 +00:14:23,820 --> 00:14:27,280 +Heart Twitch and at the time I don't think there was + +84 +00:14:27,440 --> 00:14:30,560 +anything, there was + +85 +00:14:31,060 --> 00:14:35,640 +anything like sign in with Apple even. And like I really didn't + +86 +00:14:36,140 --> 00:14:39,520 +want like to explain how harshwitch + +87 +00:14:40,020 --> 00:14:43,280 +works is you have like a watch and it will send the heart rate + +88 +00:14:43,780 --> 00:14:47,180 +to the server and then the + +89 +00:14:47,680 --> 00:14:51,100 +server will then use a web socket to push it out to a web page. + +90 +00:14:52,060 --> 00:14:55,100 +And then you would point OBS or some sort + +91 +00:14:55,600 --> 00:14:58,740 +of streaming software to the URL or to the browser window and then that way + +92 +00:14:59,240 --> 00:15:02,659 +you can stream your heart rate. That's how it works. And what I really didn't + +93 +00:15:03,159 --> 00:15:06,820 +want is a difficult way for a user to log in with + +94 +00:15:07,320 --> 00:15:10,020 +a username and password on the watch because we all know typing on the watch + +95 +00:15:10,520 --> 00:15:13,980 +is hell. So my, my thought was like, + +96 +00:15:14,320 --> 00:15:16,560 +and I didn't have sign in with Apple, right? + +97 +00:15:17,440 --> 00:15:20,880 +So my thought was why don't we use CloudKit? Because you're already signed + +98 +00:15:21,380 --> 00:15:24,080 +in a CloudKit on the Watch with your, your id. + +99 +00:15:26,640 --> 00:15:30,359 +And what you do is you log in with + +100 +00:15:30,859 --> 00:15:34,560 +a regular like email address and password in Heart Twitch on + +101 +00:15:35,060 --> 00:15:38,480 +the website. And then there's a little, there's a site, there's a part of + +102 +00:15:38,980 --> 00:15:43,060 +the site where you can sign into CloudKit and then from there + +103 +00:15:44,180 --> 00:15:47,980 +you can, because, because of the CloudKit JavaScript + +104 +00:15:48,480 --> 00:15:52,580 +library, you can then I can then pull the all + +105 +00:15:53,080 --> 00:15:55,740 +the devices because when you first launch the app on the Watch, it adds your + +106 +00:15:56,240 --> 00:15:59,740 +watch to the CloudKit database. And then I could pull that in and + +107 +00:16:00,240 --> 00:16:03,380 +then add that to my postgres database. So then there is no need for + +108 +00:16:03,880 --> 00:16:06,740 +authentication because I already have the CloudKit, + +109 +00:16:07,720 --> 00:16:11,120 +the device added in my postgres database. So it's kind of like + +110 +00:16:11,620 --> 00:16:15,520 +knows, oh yeah, this is Leo's watch, he doesn't need to authenticate. + +111 +00:16:16,020 --> 00:16:19,120 +And that way we can link devices to accounts without having to + +112 +00:16:19,620 --> 00:16:22,760 +do any sort of login process. And so this was my use case + +113 +00:16:22,919 --> 00:16:25,960 +for doing server side. + +114 +00:16:26,040 --> 00:16:29,560 +Essentially CloudKit was I could call the CloudKit web + +115 +00:16:30,060 --> 00:16:33,610 +server based + +116 +00:16:34,110 --> 00:16:37,490 +on that person's web authentication token, which we'll get + +117 +00:16:37,990 --> 00:16:40,370 +all into later. I then pull that information in. + +118 +00:16:42,050 --> 00:16:42,450 +So. + +119 +00:16:47,250 --> 00:16:47,730 +Cool. + +120 +00:16:50,770 --> 00:16:55,050 +Just checking if anybody's having issues. It doesn't look like it. So that's + +121 +00:16:55,550 --> 00:16:58,690 +good to know. So that was the private database + +122 +00:16:59,190 --> 00:17:02,990 +piece, but I actually think a much more useful case would + +123 +00:17:03,490 --> 00:17:07,150 +be the public database because the + +124 +00:17:07,650 --> 00:17:10,950 +idea would be is that you'd have some sort of app that + +125 +00:17:11,450 --> 00:17:15,790 +would use central repository of data that + +126 +00:17:16,290 --> 00:17:19,710 +it can pull information from. And I'm looking at both of these with + +127 +00:17:19,950 --> 00:17:23,310 +Bushel and then an RSS reader I'm building called Celestra + +128 +00:17:24,190 --> 00:17:27,319 +with Bushel. The. The way it's + +129 +00:17:27,819 --> 00:17:31,079 +built right now is I have this concept of hubs and + +130 +00:17:31,159 --> 00:17:34,399 +you can plug in a URL and that URL would provide or + +131 +00:17:34,899 --> 00:17:38,639 +some sort of service. That service would then provide the + +132 +00:17:39,139 --> 00:17:41,959 +Entire List of macOS restore images that are available. + +133 +00:17:44,119 --> 00:17:47,719 +But then I realized like really there's only one location for those and + +134 +00:17:48,219 --> 00:17:50,839 +each service is just going to be using the same URLs anyway. + +135 +00:17:51,970 --> 00:17:55,490 +So if I had one central repository or one central database + +136 +00:17:56,850 --> 00:18:00,250 +because they all pull from Apple, I can then parse + +137 +00:18:00,750 --> 00:18:04,530 +the web for those restore images and then store them in CloudKit and then + +138 +00:18:05,030 --> 00:18:08,770 +that way Bushel can then pull those from one + +139 +00:18:09,270 --> 00:18:12,930 +single repository. And all I would have to do, and what I'm doing now is + +140 +00:18:13,430 --> 00:18:17,130 +running basically a GitHub action or you could do like a Cron job where it + +141 +00:18:17,630 --> 00:18:21,130 +would run on Ubuntu, wouldn't even need a Mac and it would download and scrape + +142 +00:18:21,630 --> 00:18:24,430 +the web for restore images and storm in the public database. + +143 +00:18:26,350 --> 00:18:29,870 +It's the same idea with Celestra. It's an RSS reader. What if I took + +144 +00:18:30,370 --> 00:18:33,670 +those RSS RSS files + +145 +00:18:34,170 --> 00:18:37,470 +in the web and just scrape them and then store them in a CloudKit database + +146 +00:18:38,110 --> 00:18:41,310 +in a public database and then that way people can pull that + +147 +00:18:41,810 --> 00:18:42,910 +up all through CloudKit. + +148 +00:18:45,150 --> 00:18:48,550 +So the idea today is we're going to talk about how to + +149 +00:18:49,050 --> 00:18:52,380 +set something, how I set something like this up and how + +150 +00:18:52,880 --> 00:18:56,340 +you could use use my library to then go ahead and do this yourself for + +151 +00:18:56,840 --> 00:18:59,100 +any sort of work that you're going to do that where you want to use + +152 +00:18:59,600 --> 00:19:02,180 +either a public or private database in CloudKit. + +153 +00:19:03,300 --> 00:19:07,020 +So this is where I introduce myself. So I'm going to talk today about + +154 +00:19:07,520 --> 00:19:10,700 +building Miskit, which is my library I built for + +155 +00:19:11,200 --> 00:19:14,740 +doing CloudKit stuff on the server or essentially off of, + +156 +00:19:15,380 --> 00:19:17,140 +not off of Apple platforms. + +157 +00:19:19,770 --> 00:19:23,130 +Evan, do you have any questions before I keep going? No, + +158 +00:19:23,370 --> 00:19:24,890 +it's good. Good topic though. + +159 +00:19:26,810 --> 00:19:31,090 +So like I said, we have CloudKit Web Services and CloudKit + +160 +00:19:31,590 --> 00:19:35,770 +Web Services. We provide a lot of documentation. We talked about CloudKit JS + +161 +00:19:35,850 --> 00:19:39,570 +and the instructions on how to compose a web service request + +162 +00:19:40,070 --> 00:19:43,610 +which has everything I need to compose one. And back in 2020 I + +163 +00:19:44,110 --> 00:19:47,640 +did this all manually. The thing is at this + +164 +00:19:48,140 --> 00:19:51,480 +point, if you look at right there, actually if + +165 +00:19:51,980 --> 00:19:54,480 +you look at the top, you can see it hasn't been updated in over 10 + +166 +00:19:54,980 --> 00:19:58,120 +years, which is kind of crazy, + +167 +00:19:58,920 --> 00:20:02,440 +but it works. And then we + +168 +00:20:02,840 --> 00:20:06,760 +got introduced to something back in WWDC I + +169 +00:20:07,260 --> 00:20:10,840 +want to say it was 23. We got + +170 +00:20:11,340 --> 00:20:14,600 +introduced to the Open API generator which is really + +171 +00:20:15,100 --> 00:20:19,120 +nice because then we have, we can generate the Swift code + +172 +00:20:19,620 --> 00:20:23,280 +if we know what the Open API documentation looks like it. And of course + +173 +00:20:23,780 --> 00:20:27,280 +Apple doesn't provide one for CloudKit but they did provide a + +174 +00:20:27,780 --> 00:20:30,920 +pretty big piece open. If you ever you looked + +175 +00:20:31,420 --> 00:20:35,320 +at the Open API generator, it's amazing. Takes the Open API gamble file and + +176 +00:20:35,560 --> 00:20:38,880 +generates all the Swift code you need. One of the other issues + +177 +00:20:39,380 --> 00:20:43,120 +I had with first developing Miskit in 2020 + +178 +00:20:43,600 --> 00:20:47,120 +was that there was no way to like there was no abstraction + +179 +00:20:47,620 --> 00:20:51,080 +layer which could differentiate between doing something on the server or + +180 +00:20:51,580 --> 00:20:55,719 +using regular like URL session which is more targeted towards client + +181 +00:20:56,219 --> 00:20:59,880 +side. So I had + +182 +00:21:00,380 --> 00:21:02,800 +to build my own abstraction for that. Luckily Open API has, + +183 +00:21:04,080 --> 00:21:07,600 +there's open API transport I believe, which provides + +184 +00:21:08,100 --> 00:21:12,100 +an abstraction layer where you can then plug in either use Async HTTP + +185 +00:21:12,600 --> 00:21:15,660 +client, which is the server way of doing it, or you can plug in a + +186 +00:21:16,160 --> 00:21:19,380 +URL session transport, which is of course the client + +187 +00:21:19,880 --> 00:21:23,740 +way to do, provides a really great tutorial. + +188 +00:21:24,240 --> 00:21:27,740 +I highly recommend checking this out as well as the + +189 +00:21:28,240 --> 00:21:30,020 +doxy documentation that they provide. + +190 +00:21:31,860 --> 00:21:35,420 +So this is great. But then I'd have to go ahead and I'd have to + +191 +00:21:35,920 --> 00:21:39,700 +figure out a way to convert all this documentation into an open + +192 +00:21:40,200 --> 00:21:44,260 +API document. I mean, can you guess what + +193 +00:21:44,760 --> 00:21:48,260 +helped me to get build an open API document + +194 +00:21:48,760 --> 00:21:51,620 +from all this documentation? Some of the tools, + +195 +00:21:52,659 --> 00:21:54,980 +some AI tool. Yes. + +196 +00:21:56,820 --> 00:22:00,860 +AI came and I'm like, holy crap. Like AI is + +197 +00:22:01,360 --> 00:22:04,690 +really good at documenting your code, but it's also pretty darn good at taking + +198 +00:22:05,190 --> 00:22:08,450 +documentation and building code. So then I would + +199 +00:22:08,950 --> 00:22:12,290 +just plug it. I've been plugging in with Claude and it has a copy of + +200 +00:22:12,790 --> 00:22:16,490 +all the documentation in my repo and it can go ahead and edit the + +201 +00:22:16,990 --> 00:22:20,850 +open API. It's not perfect by any means, of course, but that's what unit + +202 +00:22:21,350 --> 00:22:25,770 +tests are for. And actually having integration tests + +203 +00:22:26,250 --> 00:22:31,700 +in order to do stuff so that. + +204 +00:22:35,380 --> 00:22:41,100 +Sorry, I just want to make sure nothing + +205 +00:22:46,900 --> 00:22:48,020 +I hate teams. + +206 +00:22:53,060 --> 00:22:56,420 +Okay, so great. So let's talk about. + +207 +00:22:59,700 --> 00:23:05,380 +Sorry, slides are still not done, but let's talk about authentication + +208 +00:23:05,880 --> 00:23:09,340 +methods. You can see I have the logos here, but I haven't quite cleaned + +209 +00:23:09,840 --> 00:23:14,140 +this up. So there's really two + +210 +00:23:14,640 --> 00:23:17,380 +and a half authentication methods when it comes to CloudKit. + +211 +00:23:18,420 --> 00:23:21,950 +So here is the miss demo + +212 +00:23:22,450 --> 00:23:26,070 +database. You just go in here and you can go to tokens and keys + +213 +00:23:26,570 --> 00:23:30,550 +and then that will give you access to set up either the API + +214 +00:23:31,050 --> 00:23:34,550 +if you want to do API key or API token if + +215 +00:23:35,050 --> 00:23:38,630 +you want to do a private database or a server to server keyset if + +216 +00:23:39,130 --> 00:23:41,950 +you want to do a public database. So let's talk about the API token. + +217 +00:23:42,510 --> 00:23:45,870 +Pretty simple. You just go into here, click the plus sign, + +218 +00:23:46,840 --> 00:23:49,920 +you say a name and you say whether you want to do + +219 +00:23:50,420 --> 00:23:53,920 +a post message or URL redirect. We'll get into that in a little bit in + +220 +00:23:54,420 --> 00:23:58,280 +the next section. And then whether you want to have user + +221 +00:23:58,780 --> 00:24:02,960 +info and you click save and you'll get a nice little API token + +222 +00:24:03,460 --> 00:24:06,680 +you could use in your web your web calls essentially. + +223 +00:24:09,000 --> 00:24:12,260 +API doesn't really. The API token doesn't really give you a lot of. + +224 +00:24:12,570 --> 00:24:15,330 +But what it does give you is it gives you an entry to get a + +225 +00:24:15,830 --> 00:24:19,450 +web authentication token for a user. So basically the way that + +226 +00:24:19,950 --> 00:24:22,490 +works. So you'll notice here, + +227 +00:24:23,050 --> 00:24:24,890 +when we were in this section, + +228 +00:24:27,050 --> 00:24:30,650 +we have this piece here called Sign in Callback. So you + +229 +00:24:31,150 --> 00:24:34,530 +can have either call a JavaScript, it's called a message + +230 +00:24:35,030 --> 00:24:38,730 +event, it will call a Message event and a message event will have the + +231 +00:24:39,230 --> 00:24:42,770 +metadata with the web authentication token of that user. Or you could + +232 +00:24:43,270 --> 00:24:47,250 +do URL redirect where on authentication the user has + +233 +00:24:47,750 --> 00:24:51,090 +a URL and then part of that URL is then having part of + +234 +00:24:51,590 --> 00:24:55,250 +one of the query parameters and we'll get into that. We'll then have the web + +235 +00:24:55,750 --> 00:24:59,330 +authentication token in the URL. So you + +236 +00:24:59,830 --> 00:25:03,770 +put, basically you have your website, you add the JavaScript, you need + +237 +00:25:04,330 --> 00:25:08,010 +to add the sign in with Apple. Oh, here's Josh. + +238 +00:25:14,310 --> 00:25:15,910 +Oh cool. Josh, you there? + +239 +00:25:18,790 --> 00:25:21,590 +I hope so. Good. Okay. + +240 +00:25:21,750 --> 00:25:24,429 +Hey, we were just talking about how to set up. I'm going to go back + +241 +00:25:24,929 --> 00:25:28,710 +a little bit Evan, but not too far back. Yeah, no worries. That's okay. + +242 +00:25:30,470 --> 00:25:33,790 +But we talked about setting up API token and how + +243 +00:25:34,290 --> 00:25:37,870 +to do that. So you go in + +244 +00:25:38,370 --> 00:25:41,470 +here, you just click plus, you select your sign in callback and you put in + +245 +00:25:41,970 --> 00:25:45,550 +a name and it'll give you an API token once you click + +246 +00:25:46,050 --> 00:25:46,310 +save. Basically. + +247 +00:25:50,549 --> 00:25:51,190 +Come on. + +248 +00:25:54,470 --> 00:25:58,830 +The reason you want an API token is this allows you to then have + +249 +00:25:59,330 --> 00:26:03,060 +users Sign in to CloudKit either + +250 +00:26:03,560 --> 00:26:07,540 +using, using the the web service + +251 +00:26:07,620 --> 00:26:11,380 +like Curl or you could also do it through a website using + +252 +00:26:11,880 --> 00:26:15,500 +CloudKit js. So web authentication + +253 +00:26:16,000 --> 00:26:19,260 +token we talked about how you can either do the post message or you can + +254 +00:26:19,760 --> 00:26:23,180 +do the URL redirect. Basically you have the JavaScript on + +255 +00:26:23,680 --> 00:26:26,620 +your website and there has a button, click the button, + +256 +00:26:27,120 --> 00:26:31,140 +you get this nice little window here sign in and + +257 +00:26:31,640 --> 00:26:35,020 +then when you sign in if you had selected post message, + +258 +00:26:35,340 --> 00:26:39,260 +you'll get the web authentication token and the data of the event in + +259 +00:26:39,760 --> 00:26:43,820 +JavaScript or you will get the web authentication token as a URL + +260 +00:26:44,300 --> 00:26:47,820 +in the callback URL here. Does that make sense? + +261 +00:26:50,860 --> 00:26:54,660 +Yep. Yeah. In some cases if + +262 +00:26:55,160 --> 00:26:58,520 +you scour the Internet so Stack overflow will tell you and this has happened + +263 +00:26:59,020 --> 00:27:02,360 +to me sometimes it will not be CK web authentication token, + +264 +00:27:02,860 --> 00:27:06,280 +sometimes it'll be CK session because that's what Apple likes + +265 +00:27:06,780 --> 00:27:10,120 +to do. But it's the same thing. + +266 +00:27:10,200 --> 00:27:14,160 +So you basically want to look for either property or query parameter name + +267 +00:27:14,660 --> 00:27:17,800 +and you should be good to go and then you'll have that user as well + +268 +00:27:18,300 --> 00:27:22,200 +authentication token you could do. What I, what I've + +269 +00:27:22,700 --> 00:27:27,730 +been doing is, is I've been take + +270 +00:27:28,230 --> 00:27:31,970 +like making a call to a like local server for instance and then + +271 +00:27:32,470 --> 00:27:36,330 +essentially then I could do whatever I want with that web authentication token. + +272 +00:27:36,830 --> 00:27:40,010 +As long as you have the web authentication token and the API token you can + +273 +00:27:40,510 --> 00:27:43,690 +do anything on a private database that the user has rights + +274 +00:27:44,190 --> 00:27:47,610 +to. So you can go, you can go to town with + +275 +00:27:48,110 --> 00:27:51,420 +that all this stuff gets Swift in a cookie too. + +276 +00:27:51,580 --> 00:27:54,700 +So that way it'll work. When you go back, + +277 +00:27:55,200 --> 00:27:57,500 +if you have checked the box for allow, + +278 +00:27:58,780 --> 00:28:02,180 +it's either a box or JavaScript method property that will say, hey, + +279 +00:28:02,680 --> 00:28:05,460 +I want this to persist. It'll be Swift in a, in a cookie as well. + +280 +00:28:05,960 --> 00:28:09,340 +So if you want to spelunk your cookies, you can see the web authentication + +281 +00:28:09,840 --> 00:28:13,180 +token there. So that's actually the easier of the + +282 +00:28:13,680 --> 00:28:17,300 +two. So that gives you the private database for the public database is where + +283 +00:28:17,800 --> 00:28:19,820 +you're going to need a server to server authentication. + +284 +00:28:21,340 --> 00:28:24,820 +And so to do that it's really actually not as bad + +285 +00:28:25,320 --> 00:28:28,620 +as I thought it was going to be. But you go to the new server + +286 +00:28:29,120 --> 00:28:32,500 +to server key, put in a name you want, it'll actually give you the command + +287 +00:28:33,000 --> 00:28:35,660 +you need to run and then you just paste in the public key in here. + +288 +00:28:36,380 --> 00:28:40,020 +That gives you. That will give you everything you + +289 +00:28:40,520 --> 00:28:42,780 +need. So here's how to run it. Basically, + +290 +00:28:43,990 --> 00:28:44,630 +sorry about that. + +291 +00:28:57,190 --> 00:28:59,510 +We just run that. That gives us the key. + +292 +00:29:00,710 --> 00:29:04,670 +We can go ahead and get the public key. We can also pipe + +293 +00:29:05,170 --> 00:29:08,510 +it to PB Copy and then all we have to do is paste that in + +294 +00:29:09,010 --> 00:29:10,930 +the box over here. + +295 +00:29:17,970 --> 00:29:18,690 +There we go. + +296 +00:29:25,890 --> 00:29:28,770 +It's pretty complicated to use the server key. + +297 +00:29:30,050 --> 00:29:33,450 +We can spell on the miskit code on how to do it because + +298 +00:29:33,950 --> 00:29:36,890 +it does a lot of that work for you if you have it. But you + +299 +00:29:37,390 --> 00:29:41,170 +will need the, the private key, the key id, + +300 +00:29:42,290 --> 00:29:45,490 +I think, I think that's it. And then you should be + +301 +00:29:45,990 --> 00:29:50,130 +good with having access now to the public database. + +302 +00:29:50,850 --> 00:29:54,210 +So just to go over, there's differences between the public + +303 +00:29:54,710 --> 00:29:58,050 +and private database. So this + +304 +00:29:58,550 --> 00:30:02,010 +is query. You can see my cursor, right? Query and lookup + +305 +00:30:02,510 --> 00:30:06,030 +of records is available on all but file + +306 +00:30:06,530 --> 00:30:10,150 +changes or, excuse me, record changes. It's not available on + +307 +00:30:10,650 --> 00:30:14,750 +public zones, aren't really available in public zone changes aren't available in + +308 +00:30:15,250 --> 00:30:18,870 +public notifications. Zone notifications aren't available in public, + +309 +00:30:19,670 --> 00:30:23,350 +but query notifications are. And you can also do + +310 +00:30:23,850 --> 00:30:27,310 +any stuff with assets which are basically binary files. You can + +311 +00:30:27,810 --> 00:30:32,190 +also do that in all of them. You can't do query + +312 +00:30:32,690 --> 00:30:36,110 +notifications on shared. Shared would essentially work like + +313 +00:30:36,610 --> 00:30:39,810 +private essentially. So it's just a matter + +314 +00:30:40,310 --> 00:30:42,610 +of who. Who's the owner and how is it shared. + +315 +00:30:44,690 --> 00:30:48,370 +So one of the big challenges I think we've all faced this when we've + +316 +00:30:48,870 --> 00:30:53,370 +dealt with certain web services is field type polymorphism. + +317 +00:30:53,870 --> 00:30:56,730 +If you've done JSON where you don't know what type you're getting back or what + +318 +00:30:57,230 --> 00:30:59,410 +data you're getting back, this can Be a bit challenging. + +319 +00:31:00,530 --> 00:31:03,650 +So if you look at the documentation + +320 +00:31:04,290 --> 00:31:08,290 +in Web Services Reference, there is a, + +321 +00:31:09,090 --> 00:31:12,610 +there's a page called types and dictionaries and there is + +322 +00:31:13,110 --> 00:31:16,890 +types. There's different type values for each field. If you're familiar + +323 +00:31:17,390 --> 00:31:20,650 +with CloudKit, you've seen this, right? So you have an asset + +324 +00:31:21,150 --> 00:31:25,330 +which is basically a, a binary + +325 +00:31:25,830 --> 00:31:29,650 +file. You have bytes which is + +326 +00:31:30,150 --> 00:31:33,620 +essentially a 60 byte base 64 encoded string, + +327 +00:31:34,740 --> 00:31:38,460 +date type which is returned as a number. Double is + +328 +00:31:38,960 --> 00:31:41,620 +returned as a number because These are the JavaScript types. + +329 +00:31:42,260 --> 00:31:46,140 +Int is returned as a number and then + +330 +00:31:46,640 --> 00:31:49,940 +there's location reference and then + +331 +00:31:50,020 --> 00:31:53,420 +string and list. And how would you like, + +332 +00:31:53,920 --> 00:31:57,100 +how do you do adjacent object like this? How would you + +333 +00:31:57,600 --> 00:31:59,860 +even represent this in Swift? Because you don't know what type you're going to get. + +334 +00:32:01,350 --> 00:32:04,510 +So like I said, this is a work in progress. + +335 +00:32:05,010 --> 00:32:08,710 +Sorry. So what I do, I don't know how much you can see this. + +336 +00:32:09,110 --> 00:32:13,910 +I'm going to actually move over to my documentation + +337 +00:32:14,410 --> 00:32:18,590 +here at this point. So how + +338 +00:32:19,090 --> 00:32:20,070 +are we doing on time? We good? + +339 +00:32:22,550 --> 00:32:25,590 +Yeah, I think, I think we're doing good. Okay, cool. + +340 +00:32:26,090 --> 00:32:30,240 +Any, do you want to ask questions? I don't + +341 +00:32:30,740 --> 00:32:32,160 +have anything right now. + +342 +00:32:33,760 --> 00:32:37,880 +Same nothing right now. But this seems applicable to things I'll + +343 +00:32:38,380 --> 00:32:40,480 +be doing coming up. Okay, cool. + +344 +00:32:43,200 --> 00:32:46,640 +So we have set up in the + +345 +00:32:46,800 --> 00:32:50,400 +open. So we have an open API YAML file that you can + +346 +00:32:50,900 --> 00:32:55,370 +pull up in Miskit, which is basically every like the + +347 +00:32:55,870 --> 00:32:59,290 +documentation converted to YAML. And so what we + +348 +00:32:59,790 --> 00:33:03,410 +do is you can set up in the YAML the + +349 +00:33:03,910 --> 00:33:08,330 +field value requests and they have an enum type essentially for, + +350 +00:33:12,090 --> 00:33:15,490 +for open API. So and then, + +351 +00:33:15,990 --> 00:33:18,810 +so this has, you know, it could be one of either any of these types + +352 +00:33:18,860 --> 00:33:22,090 +of. And then there's an enum in + +353 +00:33:22,590 --> 00:33:26,210 +case you have a list. So if you have a + +354 +00:33:26,710 --> 00:33:30,690 +list value type there is an extra property called + +355 +00:33:31,010 --> 00:33:33,810 +type and then that will tell you what type the. + +356 +00:33:34,450 --> 00:33:38,450 +The list is. And it's homo homomorphic. + +357 +00:33:38,690 --> 00:33:42,210 +It's all the same list type. You can't have lists of different types. + +358 +00:33:44,050 --> 00:33:49,230 +And then we have here again + +359 +00:33:49,730 --> 00:33:52,750 +field value. Sometimes the type is available, + +360 +00:33:52,910 --> 00:33:56,590 +sometimes it's not. But basically we have all the different + +361 +00:33:56,750 --> 00:33:59,950 +value types available to us in a CK value. + +362 +00:34:01,950 --> 00:34:05,670 +And then this is. Then the Open API + +363 +00:34:06,170 --> 00:34:09,150 +generator essentially builds this for me which is. + +364 +00:34:09,710 --> 00:34:13,630 +Has an enum and a struck for field field value request + +365 +00:34:15,329 --> 00:34:18,569 +and then it does all the decoding for me. Thankfully I didn't have to do + +366 +00:34:19,069 --> 00:34:19,169 +any of it. + +367 +00:34:23,089 --> 00:34:26,569 +And then yeah, I just wanted to + +368 +00:34:27,069 --> 00:34:31,969 +cover that piece where we show how we deal with these kind of like polymorphic + +369 +00:34:32,469 --> 00:34:35,969 +types and how those work. The next thing I + +370 +00:34:36,469 --> 00:34:39,929 +want to cover is error handling. So if you + +371 +00:34:40,429 --> 00:34:43,750 +look at the documentation gives you. If you get + +372 +00:34:44,250 --> 00:34:48,350 +an error we get something like this and + +373 +00:34:48,850 --> 00:34:52,030 +then that will show you in the. In the table actually shows you what + +374 +00:34:52,530 --> 00:34:56,150 +each error means. So again we do + +375 +00:34:56,650 --> 00:35:00,430 +like an enum in YAML. It's basically a string and then + +376 +00:35:00,930 --> 00:35:05,030 +we have everything else be a string. And then the open API generator will + +377 +00:35:05,530 --> 00:35:09,860 +automatically generate this which gives us the server + +378 +00:35:10,360 --> 00:35:13,980 +error code and the error response. It'll also do all this stuff + +379 +00:35:14,480 --> 00:35:18,540 +here, which is really nice. And then + +380 +00:35:18,620 --> 00:35:22,620 +we've then in our. We've abstracted a lot of this in miskit. + +381 +00:35:22,940 --> 00:35:27,100 +So that way we also have now a cloud cloud + +382 +00:35:27,600 --> 00:35:31,820 +error type which gives us a lot more info regarding that. + +383 +00:35:33,900 --> 00:35:37,360 +So that's how we handle errors. And everything I + +384 +00:35:37,860 --> 00:35:42,200 +do in the abs, the more abstract higher up stuff is done using + +385 +00:35:42,360 --> 00:35:46,360 +type throws like I have type throws and everything. So that's + +386 +00:35:46,860 --> 00:35:50,920 +how I handle that. Let me check one + +387 +00:35:51,420 --> 00:35:52,200 +last piece I wanted to cover. + +388 +00:35:54,920 --> 00:35:58,520 +The last piece I want to cover is really cool. And that is the + +389 +00:35:59,020 --> 00:36:03,160 +authentication layer. So Open API provides what's called middleware + +390 +00:36:04,440 --> 00:36:08,080 +and that allows you to, when you create a client or a server, you can + +391 +00:36:08,580 --> 00:36:11,840 +plug that in and it will handle like let's say you need to make modifications + +392 +00:36:12,340 --> 00:36:15,760 +with the request or response. When it comes in, you can intercept it + +393 +00:36:16,260 --> 00:36:17,800 +and make whatever modifications you want to make. + +394 +00:36:19,239 --> 00:36:22,880 +And in this case what we've done is I've + +395 +00:36:23,380 --> 00:36:27,840 +created an authentication middleware which + +396 +00:36:28,340 --> 00:36:31,790 +then sees if you have what's called + +397 +00:36:32,290 --> 00:36:35,630 +a token manager and an authentic you have + +398 +00:36:36,130 --> 00:36:39,910 +that and an authentication method. And the way it works is + +399 +00:36:40,410 --> 00:36:43,790 +you pick what type of authentication you want to use. If you already have like + +400 +00:36:44,290 --> 00:36:47,710 +a pre existing web token or you already have, or you, you know, + +401 +00:36:48,210 --> 00:36:51,190 +have your key ID and your private key already, or you just have the API + +402 +00:36:51,690 --> 00:36:54,870 +token. We've created basically a middleware that uses + +403 +00:36:55,370 --> 00:36:59,120 +that. So this + +404 +00:36:59,620 --> 00:37:03,320 +is how it creates the headers for server to server. So it does + +405 +00:37:03,820 --> 00:37:07,760 +all this for us. And then what + +406 +00:37:08,260 --> 00:37:11,760 +I added, which I think is really nice, is called the adaptive token manager. + +407 +00:37:12,240 --> 00:37:17,360 +And the idea with that is like let's say you're + +408 +00:37:17,860 --> 00:37:21,200 +using a client and you have the web authentication token now + +409 +00:37:21,440 --> 00:37:25,090 +and then this allows you to upgrade with that web authentication + +410 +00:37:25,590 --> 00:37:27,730 +token to the private database and have access to that. + +411 +00:37:30,530 --> 00:37:33,970 +So and then all the, all the signing is done + +412 +00:37:34,470 --> 00:37:37,650 +before you in miskit for the server to server because stuff that + +413 +00:37:38,150 --> 00:37:41,170 +needs to be signed, etc. And it takes care of all that. + +414 +00:37:41,570 --> 00:37:45,610 +All stuff that Claude was essentially able to decipher + +415 +00:37:46,110 --> 00:37:50,060 +from the documentation. + +416 +00:37:52,620 --> 00:37:54,300 +There's one more thing I wanted to show. + +417 +00:37:56,380 --> 00:38:00,220 +If you want to hop in with a question while I pull something up, + +418 +00:38:00,300 --> 00:38:00,940 +feel free. + +419 +00:38:21,190 --> 00:38:24,390 +No questions. Cool. + +420 +00:38:24,790 --> 00:38:28,630 +So I'm going to show one last thing and that is how + +421 +00:38:28,710 --> 00:38:30,310 +do we actually deploy this? + +422 +00:38:33,350 --> 00:38:36,950 +Is this too big, too small? Looks okay. + +423 +00:38:37,590 --> 00:38:40,070 +That looks good. Yeah, it looks good. Okay, cool. + +424 +00:38:43,850 --> 00:38:47,890 +So essentially what I've done is I'm using GitHub + +425 +00:38:48,390 --> 00:38:50,410 +Actions. There's a way you can. + +426 +00:38:53,130 --> 00:38:56,689 +This is all public by the way, so I will provide + +427 +00:38:57,189 --> 00:39:00,570 +URLs in the Slack or something. Let's do this one. + +428 +00:39:02,410 --> 00:39:07,220 +So this is a Swift package for + +429 +00:39:07,720 --> 00:39:10,660 +Bushel. It's called Bushel Cloud. It pulls the stuff up from. + +430 +00:39:11,220 --> 00:39:14,740 +Uses Miskit to go ahead and + +431 +00:39:16,740 --> 00:39:20,340 +pull, get access to CloudKit and + +432 +00:39:21,060 --> 00:39:24,860 +let me go back to the workflow. How familiar + +433 +00:39:25,360 --> 00:39:26,580 +are you with GitHub workflows? + +434 +00:39:29,860 --> 00:39:32,980 +Sadly not had the chance to work too deeply with them yet. + +435 +00:39:33,690 --> 00:39:37,490 +Okay. Basically it's like for CI, but you can also set + +436 +00:39:37,990 --> 00:39:41,850 +it up on a schedule. So I did that and then + +437 +00:39:42,890 --> 00:39:46,490 +it runs the scheduled job and then I just execute. + +438 +00:39:50,650 --> 00:39:54,650 +So then this was refactored over here into + +439 +00:39:55,150 --> 00:39:58,490 +an action. There we go. + +440 +00:39:59,540 --> 00:40:03,460 +And I have all sorts of stuff here for + +441 +00:40:05,380 --> 00:40:10,300 +like this is generic essentially, but all + +442 +00:40:10,800 --> 00:40:14,220 +these, the environment, etc. These are all passed from + +443 +00:40:14,720 --> 00:40:17,980 +that workflow into here. These are basically either API keys + +444 +00:40:18,480 --> 00:40:22,100 +or the information that I need for accessing Cloud, the public, + +445 +00:40:24,020 --> 00:40:28,120 +public database. Right. And then I + +446 +00:40:28,620 --> 00:40:31,880 +already pre built the binary. So we + +447 +00:40:32,380 --> 00:40:35,960 +already have that. We're running this on Ubuntu because + +448 +00:40:36,460 --> 00:40:40,280 +it's the default. Look at it. If there + +449 +00:40:40,780 --> 00:40:43,840 +is no binary, it goes ahead and builds the binary for me. + +450 +00:40:44,000 --> 00:40:45,200 +So that's what this is doing. + +451 +00:40:47,120 --> 00:40:50,640 +And then we make sure the binary works. + +452 +00:40:50,880 --> 00:40:54,450 +We make, we make it executable, we validate, make sure all the + +453 +00:40:55,010 --> 00:40:58,690 +API secrets are there. We then go ahead + +454 +00:40:58,930 --> 00:41:02,370 +and this validates the pim. But essentially this is the fun part. + +455 +00:41:03,410 --> 00:41:06,770 +We go ahead, we have all our inputs for the private key, + +456 +00:41:07,270 --> 00:41:09,570 +the key id, environment, container id. + +457 +00:41:10,610 --> 00:41:13,410 +And then I use Virtual Buddy for signing verification. + +458 +00:41:14,050 --> 00:41:14,450 +And. + +459 +00:41:18,460 --> 00:41:21,940 +It then goes in and it runs the + +460 +00:41:22,440 --> 00:41:25,660 +sync and then we'll go in. + +461 +00:41:25,980 --> 00:41:29,500 +Basically it pulls from several websites information + +462 +00:41:29,580 --> 00:41:32,939 +about macrosos, restore images and checks whether they're signed. + +463 +00:41:33,340 --> 00:41:37,540 +And then it goes ahead and it adds those to + +464 +00:41:38,040 --> 00:41:41,780 +the database. And then what this does is it exports the information in + +465 +00:41:42,280 --> 00:41:44,580 +a run. Let's, let's take a look, see if I have one. I can show + +466 +00:41:45,080 --> 00:41:47,420 +you. Oh, there's one scheduled. + +467 +00:41:50,060 --> 00:41:53,700 +Yeah, here we go. So there's 57 + +468 +00:41:54,200 --> 00:41:55,580 +new restore images created, + +469 +00:41:56,300 --> 00:41:58,300 +177 updated. + +470 +00:41:58,780 --> 00:42:02,300 +234 total. No operations + +471 +00:42:02,380 --> 00:42:05,900 +failed. I also store Xcode versions and Swift versions. + +472 +00:42:06,780 --> 00:42:10,460 +Those get stored as well. Had to rebuild it, + +473 +00:42:10,630 --> 00:42:11,830 +but here is the results. + +474 +00:42:13,750 --> 00:42:17,750 +I'm not going to pull that up, but it's essentially updated + +475 +00:42:18,250 --> 00:42:22,470 +my CloudKit database and + +476 +00:42:22,550 --> 00:42:25,870 +that's all in the public database. And then maybe even by + +477 +00:42:26,370 --> 00:42:29,910 +the time I present this, I'll have a working example in Bushel with that example + +478 +00:42:30,410 --> 00:42:33,750 +working, which would be awesome. Celestra, + +479 +00:42:33,990 --> 00:42:37,190 +same idea. So this looks like it was a RSS update. + +480 +00:42:38,910 --> 00:42:42,830 +We get the workflow file and. + +481 +00:42:43,330 --> 00:42:46,110 +Oh, sorry, I should point out, because you're probably wondering where is all these. + +482 +00:42:46,610 --> 00:42:50,150 +The stuff all these secrets stored? Yes, they are stored in + +483 +00:42:50,650 --> 00:42:53,910 +Actions secrets right here. So we have + +484 +00:42:54,410 --> 00:42:58,190 +our private key ID API key from + +485 +00:42:58,690 --> 00:43:01,230 +Virtual Buddy. So that's all stored there. + +486 +00:43:01,870 --> 00:43:05,830 +Here is Celestra. It's for updating RSS + +487 +00:43:06,330 --> 00:43:09,930 +feeds. So it just basically goes through. You can look at the Swift code + +488 +00:43:10,430 --> 00:43:14,490 +it goes through, pulls RSS feeds and updates them into a CloudKit + +489 +00:43:15,530 --> 00:43:18,490 +record or what do you call it? Yeah, record type. + +490 +00:43:19,850 --> 00:43:22,210 +And I of course try to do it in such a way not to hammer + +491 +00:43:22,710 --> 00:43:24,170 +people, but same idea, + +492 +00:43:27,050 --> 00:43:30,610 +yeah, it goes ahead and it runs the + +493 +00:43:31,110 --> 00:43:35,890 +binary it updates and then I also have like actual parameters + +494 +00:43:36,390 --> 00:43:40,170 +that I take to to filter out, like which RSS feeds are high priority + +495 +00:43:40,670 --> 00:43:44,330 +and which ones aren't based on the audience and etc. So yeah, + +496 +00:43:44,890 --> 00:43:48,410 +so that's deployment. That's how you can get that working. + +497 +00:43:48,810 --> 00:43:53,130 +There's weird stuff with cloud with GitHub that + +498 +00:43:53,690 --> 00:43:57,210 +I've noticed. If you haven't updated it in a while, it doesn't run these + +499 +00:43:57,710 --> 00:43:59,570 +cron jobs. So I need to figure out a how to get around it or + +500 +00:44:00,070 --> 00:44:04,030 +find another service to do it. This is all free because + +501 +00:44:04,110 --> 00:44:07,870 +it's public and it is running + +502 +00:44:08,370 --> 00:44:09,870 +on Ubuntu. So that's really great. + +503 +00:44:12,350 --> 00:44:16,070 +And the storage on CloudKit is dirt cheap, which is even + +504 +00:44:16,570 --> 00:44:16,830 +more awesome. + +505 +00:44:20,030 --> 00:44:23,990 +Sorry, let's see what else. I just + +506 +00:44:24,490 --> 00:44:27,150 +want to make sure I covered all my slides. The last thing I'm going to + +507 +00:44:27,650 --> 00:44:28,670 +talk about is just what are my plans? + +508 +00:44:30,390 --> 00:44:33,390 +Excuse me. So I don't know if you check. Follow me. + +509 +00:44:33,890 --> 00:44:34,550 +But I just released. + +510 +00:44:41,910 --> 00:44:45,750 +I just released Alpha 5 that has lookup zones, + +511 +00:44:46,250 --> 00:44:50,150 +fetch, record changes and upload assets. Upload the assets is pretty awesome. + +512 +00:44:50,230 --> 00:44:53,150 +When I saw that work because I was like, cool, I can actually upload a + +513 +00:44:53,650 --> 00:44:57,630 +binary to CloudKit, which is awesome. We got + +514 +00:44:58,130 --> 00:45:01,790 +query filters to work for in and not in, so you could do that I + +515 +00:45:02,290 --> 00:45:05,510 +have plans to continue working on this because I think there's a big future for + +516 +00:45:06,010 --> 00:45:09,590 +something like this for a lot of people. Yes, + +517 +00:45:10,090 --> 00:45:13,950 +you can technically use this in Android or Windows because the Swift + +518 +00:45:14,270 --> 00:45:17,670 +thing does compile in Android and Windows. You can see I already added support for + +519 +00:45:18,170 --> 00:45:22,360 +that. This is the support I recently had. And then we're. + +520 +00:45:22,860 --> 00:45:25,880 +I'm just kind of like going through each of these because as great as AI + +521 +00:45:26,380 --> 00:45:30,120 +is, it's not perfect. So we're just kind of going through these piece + +522 +00:45:30,620 --> 00:45:35,720 +by piece with each version and hammering these away and + +523 +00:45:36,220 --> 00:45:40,160 +then this is actually done. I don't even know why that's there. But yeah, + +524 +00:45:40,660 --> 00:45:43,960 +I think system field integration might already be there and there's + +525 +00:45:44,460 --> 00:45:48,120 +a few other things. Eventually I'd like to add support. + +526 +00:45:48,200 --> 00:45:52,880 +So there, there's a whole API for CloudKit schema management that + +527 +00:45:53,380 --> 00:45:55,720 +I could. That would be awesome if I could figure out how to do that. + +528 +00:45:56,220 --> 00:45:58,640 +If I could figure out how to do key path query filtering, that would be + +529 +00:45:59,140 --> 00:46:02,760 +fantastic. And yeah, + +530 +00:46:03,260 --> 00:46:06,080 +but there's a. I mean the basics is there as far as if you want + +531 +00:46:06,580 --> 00:46:09,080 +to do anything with a record, it's pretty much there. + +532 +00:46:09,720 --> 00:46:13,160 +One thing with Celestra is I'd love to be able to do like test out + +533 +00:46:13,660 --> 00:46:17,840 +subscriptions and see how that works. So yeah, + +534 +00:46:18,340 --> 00:46:20,040 +that's really the bulk of my presentation today. + +535 +00:46:21,800 --> 00:46:24,880 +Now is. Now it's time to ask me a ton of questions and make me + +536 +00:46:25,380 --> 00:46:26,600 +feel dumb. Go for it. + +537 +00:46:28,440 --> 00:46:32,160 +No, there's a lot there to. To absorb. + +538 +00:46:32,660 --> 00:46:36,000 +But I, I like the concept and I know you've been working on this + +539 +00:46:36,500 --> 00:46:39,720 +for a while and I always thought it was a pretty cool, pretty cool + +540 +00:46:40,030 --> 00:46:42,190 +idea and implementation of this. + +541 +00:46:42,750 --> 00:46:43,470 +Questions? + +542 +00:46:48,990 --> 00:46:50,030 +So with something like. + +543 +00:46:54,110 --> 00:46:58,110 +Accessing CloudKit through the web, is this setup more + +544 +00:46:58,610 --> 00:47:02,270 +ideal for having your server do + +545 +00:47:02,670 --> 00:47:06,650 +the authentication to CloudKit with Miskit or is + +546 +00:47:07,150 --> 00:47:10,530 +miskit something that you could put into even like a client side, + +547 +00:47:12,850 --> 00:47:17,010 +you know, like non Swift application or + +548 +00:47:17,510 --> 00:47:20,970 +I guess not non Swift but like non like app application. I'm thinking in + +549 +00:47:21,470 --> 00:47:22,049 +the context of like a. + +550 +00:47:25,730 --> 00:47:30,290 +I guess if I wanted to create a something + +551 +00:47:30,790 --> 00:47:33,410 +accessing CloudKit that is not your typical Mac or iOS app. + +552 +00:47:34,880 --> 00:47:36,160 +Can you be more specific? + +553 +00:47:37,840 --> 00:47:42,040 +I'm looking into one. One approach would be browser + +554 +00:47:42,540 --> 00:47:46,000 +extensions. So for + +555 +00:47:46,500 --> 00:47:48,240 +like a non Safari browser. Yes. + +556 +00:47:50,400 --> 00:47:54,120 +Yeah, this would be great. So basically the way you'd want + +557 +00:47:54,620 --> 00:47:58,240 +that to work, like the sticky part to me would be getting the web authentication + +558 +00:47:58,740 --> 00:48:01,090 +token. Other than that, like have at it. + +559 +00:48:04,610 --> 00:48:07,810 +So I'm gonna, I'm gonna be devil's advocate. Why not just use + +560 +00:48:08,310 --> 00:48:11,490 +the CloudKit JavaScript library. If it's an extension, + +561 +00:48:12,450 --> 00:48:14,290 +my brain jumps to Swift first. + +562 +00:48:15,730 --> 00:48:18,930 +Right. But it's the reason I'm asking that is like it's a, + +563 +00:48:19,410 --> 00:48:23,490 +it's already a web extension. I would assume that is true. That it's + +564 +00:48:23,990 --> 00:48:26,290 +90 web based or JavaScript based. + +565 +00:48:27,120 --> 00:48:30,560 +So that's where I'm just like, well, you may as well. Like, I would love. + +566 +00:48:30,640 --> 00:48:33,600 +I don't want to. Like, I love tooting my own horn. Right. But like, + +567 +00:48:34,880 --> 00:48:37,120 +like why not just. Unless you're. + +568 +00:48:40,720 --> 00:48:43,840 +Unless you're like building a executable, + +569 +00:48:44,160 --> 00:48:45,920 +I guess, or an app. Ish. + +570 +00:48:47,760 --> 00:48:52,040 +And I guess another application for this would be doing + +571 +00:48:52,540 --> 00:48:56,280 +CloudKit stuff server side and then providing my own API layer + +572 +00:48:56,780 --> 00:48:59,860 +over it. Yep, yep. So that's. + +573 +00:49:00,360 --> 00:49:03,740 +Yeah. Are we talking private database or public database? Private. + +574 +00:49:05,580 --> 00:49:09,140 +So in that case, basically like you'd have to go + +575 +00:49:09,640 --> 00:49:13,380 +the Hard Twitch route and you would have to + +576 +00:49:13,880 --> 00:49:17,420 +provide a way to get their web authentication + +577 +00:49:17,920 --> 00:49:21,260 +token, essentially, if that makes sense. And then store + +578 +00:49:21,760 --> 00:49:24,140 +it in Postgres or whatever the hell you want to do. Like that's, that's the + +579 +00:49:24,640 --> 00:49:27,520 +way I did it with Hard Twitch. But once you have that, you can do + +580 +00:49:28,020 --> 00:49:31,200 +anything you want on the server with their private database, + +581 +00:49:31,700 --> 00:49:34,480 +if that makes sense. It does. Yep. + +582 +00:49:34,560 --> 00:49:38,240 +Yep. A couple of things I wanted to bring up, + +583 +00:49:38,320 --> 00:49:39,520 +so let's take a look. + +584 +00:49:44,000 --> 00:49:47,400 +So part of my + +585 +00:49:47,900 --> 00:49:51,880 +other presentation is working, talking about cross + +586 +00:49:52,380 --> 00:49:56,760 +platform automation type stuff. And the + +587 +00:49:57,260 --> 00:50:00,680 +one issue I've run into is. So it basically builds on everything. + +588 +00:50:00,920 --> 00:50:01,560 +Right now. + +589 +00:50:07,560 --> 00:50:10,520 +I'm going to share something. Hey guys, + +590 +00:50:11,000 --> 00:50:14,680 +I got to drop. But it was good presentation, Leo. Thank you. + +591 +00:50:14,840 --> 00:50:17,640 +Yeah, yeah. If I have more questions, if you have any feedback, just hit me + +592 +00:50:18,140 --> 00:50:21,590 +up on Slack. Sounds good. Cool, thank you. Thank you so much + +593 +00:50:22,090 --> 00:50:25,910 +for helping me set this up. Yeah, talk to you later. Thank you. Bye bye. + +594 +00:50:28,870 --> 00:50:31,430 +Yeah, so if you had something else to show, I'm happy to look for. + +595 +00:50:31,930 --> 00:50:34,390 +I'm here for a few more minutes as well. Yeah, yeah, yeah. + +596 +00:50:38,790 --> 00:50:42,070 +So I have the workflow working here and it + +597 +00:50:42,570 --> 00:50:46,120 +does Ubuntu, it does Windows, it does Android. + +598 +00:50:46,620 --> 00:50:50,920 +So all that stuff is available to you. I would never recommend using Miskit + +599 +00:50:51,420 --> 00:50:54,240 +on an Apple platform for obvious reasons, like what's the point? + +600 +00:50:55,600 --> 00:50:59,360 +True. Unless there's something special that I provide that CloudKit doesn't like, + +601 +00:50:59,440 --> 00:51:03,520 +I don't get it. Right. But we have an + +602 +00:51:04,020 --> 00:51:07,640 +issue. So I just started dabbling. I haven't really done anything + +603 +00:51:08,140 --> 00:51:11,730 +with wasm, but I did definitely try. Like I added support for + +604 +00:51:12,230 --> 00:51:14,890 +WASM in my, in my Swift build action. + +605 +00:51:17,210 --> 00:51:21,050 +The thing about WASA is it does not provide. It doesn't have a transport + +606 +00:51:21,130 --> 00:51:24,410 +available. So we talked about transports, + +607 +00:51:26,010 --> 00:51:30,090 +I think. Did you hear about that part about the Open API generator and transports? + +608 +00:51:31,370 --> 00:51:33,690 +I think I was coming in at that point. + +609 +00:51:34,410 --> 00:51:36,670 +Okay. When you create a client, + +610 +00:51:37,630 --> 00:51:42,630 +so underneath the client you + +611 +00:51:43,130 --> 00:51:46,990 +have what's called a client transport. This is so underneath this + +612 +00:51:47,490 --> 00:51:51,270 +client, this is an abstraction layer above. So this is not the right + +613 +00:51:51,770 --> 00:51:53,390 +one. Where's the public one? + +614 +00:52:00,680 --> 00:52:05,440 +But anyway, there is here + +615 +00:52:05,940 --> 00:52:06,920 +CloudKit service maybe. + +616 +00:52:09,560 --> 00:52:13,640 +Yeah, here we go. So the CloudKit service has + +617 +00:52:14,140 --> 00:52:17,960 +a client and part of the client is being able + +618 +00:52:19,960 --> 00:52:23,560 +to say what transport you use in Open API. + +619 +00:52:24,760 --> 00:52:29,330 +And there's + +620 +00:52:29,830 --> 00:52:33,730 +two transports available right now. One is, + +621 +00:52:36,850 --> 00:52:40,210 +one is your regular URL session for clients, which. + +622 +00:52:40,710 --> 00:52:43,810 +That makes sense. Right. And then there's the Async HTTP + +623 +00:52:44,310 --> 00:52:47,970 +client which is typically used like Swift NEO based for servers. + +624 +00:52:49,330 --> 00:52:53,170 +The thing is that neither of those are available in wasp. + +625 +00:52:54,290 --> 00:52:57,810 +Do you know what WASM is? I have no experience with it, but yes. + +626 +00:52:58,850 --> 00:53:02,290 +Okay. It's. It's the web browser. Right. So. + +627 +00:53:02,690 --> 00:53:04,850 +So you really can't use Miskit in. + +628 +00:53:06,450 --> 00:53:10,210 +In the. In WASM yet because there is no transport. Now having said that, + +629 +00:53:10,530 --> 00:53:12,450 +why on earth would you use. + +630 +00:53:13,090 --> 00:53:16,970 +Awesome. Why would you use Miskit in the browser? Why not just use CloudKit + +631 +00:53:17,470 --> 00:53:20,700 +js? So that's essentially, + +632 +00:53:21,580 --> 00:53:22,060 +you know, + +633 +00:53:29,260 --> 00:53:30,940 +What other questions do you have? + +634 +00:53:35,660 --> 00:53:41,340 +My brain is mushy right now, so because + +635 +00:53:41,840 --> 00:53:45,450 +of my presentation or because other things, I got two hours of + +636 +00:53:45,950 --> 00:53:50,170 +sleep. Oh, I'm so sorry. So I'm + +637 +00:53:50,670 --> 00:53:51,450 +following as best as I can. + +638 +00:53:54,330 --> 00:53:57,410 +Snuggling. Yeah, + +639 +00:53:57,910 --> 00:54:01,410 +the intro was basically how I originally built it + +640 +00:54:01,910 --> 00:54:06,210 +for hard Twitch in 2020 for a private database login for + +641 +00:54:06,710 --> 00:54:09,210 +the Apple Watch because I don't want to have a login screen. And so basically + +642 +00:54:09,710 --> 00:54:12,490 +there's a way in the web browser to link your Apple Watch to your account + +643 +00:54:12,990 --> 00:54:16,280 +and then from there you don't need to authenticate anymore. Nice. I built + +644 +00:54:16,780 --> 00:54:20,280 +that all from hand and then in 23 they + +645 +00:54:20,780 --> 00:54:24,720 +came out with the Open API generator which was like, oh wait, what if + +646 +00:54:24,800 --> 00:54:28,160 +I can create an open API file out of + +647 +00:54:28,320 --> 00:54:30,800 +Apple's 10 year old documentation? + +648 +00:54:33,120 --> 00:54:36,280 +That'd be a lot of work, but I could do it. And I + +649 +00:54:36,780 --> 00:54:40,480 +don't know if you heard, but there was this thing that came out a + +650 +00:54:40,980 --> 00:54:45,340 +couple years ago called AI and it's + +651 +00:54:45,840 --> 00:54:49,140 +really good at creating documentation for your code, but it's also really good at creating + +652 +00:54:49,640 --> 00:54:53,940 +code for your documentation. And so I was like, oh yeah, + +653 +00:54:54,440 --> 00:54:57,900 +this is great. Like I can just, I can just Feed it + +654 +00:54:58,400 --> 00:55:01,940 +the documentation and go from there. + +655 +00:55:02,020 --> 00:55:05,140 +And, like, basically, I've been going step by step through. + +656 +00:55:05,940 --> 00:55:09,300 +Like I said, if you looked at the miskit repo, + +657 +00:55:09,800 --> 00:55:14,620 +like, I'm going through step by step and adding new APIs based + +658 +00:55:15,120 --> 00:55:18,180 +on what's available in the documentation, piece by piece. And I would say at this + +659 +00:55:18,680 --> 00:55:22,420 +point, it's like most of the really, like 80% of that people use + +660 +00:55:22,920 --> 00:55:26,700 +is there. There's like, stuff like subscriptions and zones that I'm still trying + +661 +00:55:27,200 --> 00:55:30,940 +to figure out, but it's. It's pretty close to done at this point. + +662 +00:55:31,260 --> 00:55:31,900 +Mm. + +663 +00:55:35,110 --> 00:55:38,590 +If you use it. Yeah, it's one of those. Because I. Go ahead. + +664 +00:55:39,090 --> 00:55:41,070 +Yeah. I was gonna say it's one of those projects that makes me want to + +665 +00:55:41,570 --> 00:55:45,110 +set up a. Like a vapor server or something just to do some Swift on + +666 +00:55:45,610 --> 00:55:48,150 +the server. Yeah. Or just like, + +667 +00:55:48,870 --> 00:55:52,470 +I wonder if there's like, something you do on a pie, like just + +668 +00:55:52,970 --> 00:55:55,350 +hook it up to a CloudKit database. Like, there's a lot you could do here + +669 +00:55:55,850 --> 00:55:57,510 +because all you need is decent os. + +670 +00:55:58,950 --> 00:56:02,110 +I don't know anything about sharing. I haven't done anything with sharing yet, + +671 +00:56:02,610 --> 00:56:05,180 +so I still have to do that and a few other things, but. No, + +672 +00:56:05,680 --> 00:56:08,940 +yeah, it's an interesting idea. + +673 +00:56:09,900 --> 00:56:12,860 +Thank you. Yeah. Well, thank you for joining, + +674 +00:56:13,360 --> 00:56:17,340 +Josh. Yeah. Thanks for hosting this and sharing this info. It's nice. + +675 +00:56:18,060 --> 00:56:20,780 +Yeah. If you ever run into anything, let me know. + +676 +00:56:21,420 --> 00:56:24,780 +Will do. All right, talk to you later. All right, + +677 +00:56:25,280 --> 00:56:26,700 +sounds good. See you. Bye. + diff --git a/docs/transcriptions/transcript.txt b/docs/transcriptions/transcript.txt new file mode 100644 index 00000000..408179fe --- /dev/null +++ b/docs/transcriptions/transcript.txt @@ -0,0 +1,177 @@ +Speaker A: Hey, Evan, can you hear me all right? + +Speaker B: Yeah, I can hear you. + +Speaker A: Awesome. How do I sound? Good. I've used this microphone in ages. It's like all dusty. How you think I should wait like five minutes for people to come in or. + +Speaker B: Probably. Yeah, that there's if. Yeah, otherwise you can just. You could start, but that'll be interesting. + +Speaker A: Do you mind if I grab a cup of coffee real quick? + +Speaker B: No, not at all. + +Speaker A: Not at all. Okay, cool. I'm not using the AirPods mic, so I can hear you, but you won't be able to hear me. + +Speaker B: Okay. + +Speaker A: It's. Thank you for your patience. So is it just you? + +Speaker B: It looks like it's just me. Josh is trying to get in, but he's trying to get on on his mobile device and I don't think that's possible with Riverside. + +Speaker A: Surprised? I mean, I know they have an app. + +Speaker B: Maybe he's using. I'm not sure if he's using. Using the app or not. + +Speaker A: Okay. Should I just go? + +Speaker B: Sure. + +Speaker A: Okay. Well, thanks for joining me, Evan. I really appreciate it. I would say no. I mean I do, seriously. So yeah, this is a kind of a dry run. I would say I'm about 60% done with this presentation about CloudKit on the server and we'll probably hop back and forth between Keynote and not Keynote, but yeah. So this is CloudKit as your backend from iOS to server side Swift. So what is CloudKit? CloudKit is a service launched by Apple probably a decade ago to kind of give developers a built in back end for storing data for their apps. One of the biggest benefits is is how cheap it is to use for iOS developers. So if you have built an app, you could just add CloudKit right here within the Xcode project and use the regular CloudKit API in Swift to go ahead and start using it in your app. Here is what it looks like to create a new record type. You can do all this through the CloudKit dashboard. In CloudKit you could also do this using a schema file too. And you can export and import your schema that way. And it's not a SQL based database, it's much more, no sequel ish or an abstract layer above it. But essentially you can create records kind of like a table but not quite in your records. You can create a struct for it. You can just use CloudKit directly to go ahead and then you can then plug it into your app and do fun stuff like this. We can do things like queries and basic database stuff. There's a lot of advantages to it. For one, if you're doing Apple only, then it definitely makes sense to look into, at least look into CloudKit. If you're just going to deploy to Apple Devices. If you don't mind the, the fact that it's not a regular SQL database, that's something too to think about. If you like need a SQL database, this might not be what you want. And then if you don't mind working with a lot of the abstraction layers that CloudKit provides, then this might be good for you to get started or especially if you don't have any database experience. So as far as like server choices, I would say CloudKit might not be your first choice, but it certainly is a decent choice if you're going the Apple only route. But then the question comes in, why would you want Cloud server side CloudKit? Why would you want to do anything with CloudKit on the server? So here's, here's the first case. Well, this is how you can go ahead and do that is they provide actually a REST API for calls to CloudKit using the, if you go to the documentation, I'll provide a link to that CloudKit Web Services which provides a lot of the documentation for what we'll be talking about today. A lot of this is abstracted out in the JavaScript library. So if you want to do stuff on a website, they provide a CloudKit JavaScript library for that. Sorry, just going into do not disturb mode. They even in that web references documentation they provide a composing web service request and all these instructions about how to go ahead and do that. So man, was it like half a decade ago that I built Heart Twitch and at the time I don't think there was anything, there was anything like sign in with Apple even. And like I really didn't want like to explain how harshwitch works is you have like a watch and it will send the heart rate to the server and then the server will then use a web socket to push it out to a web page. And then you would point OBS or some sort of streaming software to the URL or to the browser window and then that way you can stream your heart rate. That's how it works. And what I really didn't want is a difficult way for a user to log in with a username and password on the watch because we all know typing on the watch is hell. So my, my thought was like, and I didn't have sign in with Apple, right? So my thought was why don't we use CloudKit? Because you're already signed in a CloudKit on the Watch with your, your id. And what you do is you log in with a regular like email address and password in Heart Twitch on the website. And then there's a little, there's a site, there's a part of the site where you can sign into CloudKit and then from there you can, because, because of the CloudKit JavaScript library, you can then I can then pull the all the devices because when you first launch the app on the Watch, it adds your watch to the CloudKit database. And then I could pull that in and then add that to my postgres database. So then there is no need for authentication because I already have the CloudKit, the device added in my postgres database. So it's kind of like knows, oh yeah, this is Leo's watch, he doesn't need to authenticate. And that way we can link devices to accounts without having to do any sort of login process. And so this was my use case for doing server side. Essentially CloudKit was I could call the CloudKit web server based on that person's web authentication token, which we'll get all into later. I then pull that information in. So. Cool. Just checking if anybody's having issues. It doesn't look like it. So that's good to know. So that was the private database piece, but I actually think a much more useful case would be the public database because the idea would be is that you'd have some sort of app that would use central repository of data that it can pull information from. And I'm looking at both of these with Bushel and then an RSS reader I'm building called Celestra with Bushel. The. The way it's built right now is I have this concept of hubs and you can plug in a URL and that URL would provide or some sort of service. That service would then provide the Entire List of macOS restore images that are available. But then I realized like really there's only one location for those and each service is just going to be using the same URLs anyway. So if I had one central repository or one central database because they all pull from Apple, I can then parse the web for those restore images and then store them in CloudKit and then that way Bushel can then pull those from one single repository. And all I would have to do, and what I'm doing now is running basically a GitHub action or you could do like a Cron job where it would run on Ubuntu, wouldn't even need a Mac and it would download and scrape the web for restore images and storm in the public database. It's the same idea with Celestra. It's an RSS reader. What if I took those RSS RSS files in the web and just scrape them and then store them in a CloudKit database in a public database and then that way people can pull that up all through CloudKit. So the idea today is we're going to talk about how to set something, how I set something like this up and how you could use use my library to then go ahead and do this yourself for any sort of work that you're going to do that where you want to use either a public or private database in CloudKit. So this is where I introduce myself. So I'm going to talk today about building Miskit, which is my library I built for doing CloudKit stuff on the server or essentially off of, not off of Apple platforms. Evan, do you have any questions before I keep going? + +Speaker B: No, it's good. Good topic though. + +Speaker A: So like I said, we have CloudKit Web Services and CloudKit Web Services. We provide a lot of documentation. We talked about CloudKit JS and the instructions on how to compose a web service request which has everything I need to compose one. And back in 2020 I did this all manually. The thing is at this point, if you look at right there, actually if you look at the top, you can see it hasn't been updated in over 10 years, which is kind of crazy, but it works. And then we got introduced to something back in WWDC I want to say it was 23. We got introduced to the Open API generator which is really nice because then we have, we can generate the Swift code if we know what the Open API documentation looks like it. And of course Apple doesn't provide one for CloudKit but they did provide a pretty big piece open. If you ever you looked at the Open API generator, it's amazing. Takes the Open API gamble file and generates all the Swift code you need. One of the other issues I had with first developing Miskit in 2020 was that there was no way to like there was no abstraction layer which could differentiate between doing something on the server or using regular like URL session which is more targeted towards client side. So I had to build my own abstraction for that. Luckily Open API has, there's open API transport I believe, which provides an abstraction layer where you can then plug in either use Async HTTP client, which is the server way of doing it, or you can plug in a URL session transport, which is of course the client way to do, provides a really great tutorial. I highly recommend checking this out as well as the doxy documentation that they provide. So this is great. But then I'd have to go ahead and I'd have to figure out a way to convert all this documentation into an open API document. I mean, can you guess what helped me to get build an open API document from all this documentation? + +Speaker B: Some of the tools, some AI tool. + +Speaker A: Yes. AI came and I'm like, holy crap. Like AI is really good at documenting your code, but it's also pretty darn good at taking documentation and building code. So then I would just plug it. I've been plugging in with Claude and it has a copy of all the documentation in my repo and it can go ahead and edit the open API. It's not perfect by any means, of course, but that's what unit tests are for. And actually having integration tests in order to do stuff so that. Sorry, I just want to make sure nothing important. I hate teams. Okay, so great. So let's talk about. Sorry, slides are still not done, but let's talk about authentication methods. You can see I have the logos here, but I haven't quite cleaned this up. So there's really two and a half authentication methods when it comes to CloudKit. So here is the miss demo database. You just go in here and you can go to tokens and keys and then that will give you access to set up either the API if you want to do API key or API token if you want to do a private database or a server to server keyset if you want to do a public database. So let's talk about the API token. Pretty simple. You just go into here, click the plus sign, you say a name and you say whether you want to do a post message or URL redirect. We'll get into that in a little bit in the next section. And then whether you want to have user info and you click save and you'll get a nice little API token you could use in your web your web calls essentially. API doesn't really. The API token doesn't really give you a lot of. But what it does give you is it gives you an entry to get a web authentication token for a user. So basically the way that works. So you'll notice here, when we were in this section, we have this piece here called Sign in Callback. So you can have either call a JavaScript, it's called a message event, it will call a Message event and a message event will have the metadata with the web authentication token of that user. Or you could do URL redirect where on authentication the user has a URL and then part of that URL is then having part of one of the query parameters and we'll get into that. We'll then have the web authentication token in the URL. So you put, basically you have your website, you add the JavaScript, you need to add the sign in with Apple. Oh, here's Josh. Oh cool. Josh, you there? + +Speaker C: I hope so. + +Speaker A: Good. Okay. Hey, we were just talking about how to set up. I'm going to go back a little bit Evan, but not too far back. + +Speaker B: Yeah, no worries. + +Speaker A: That's okay. But we talked about setting up API token and how to do that. So you go in here, you just click plus, you select your sign in callback and you put in a name and it'll give you an API token once you click save. Basically. Come on. The reason you want an API token is this allows you to then have users Sign in to CloudKit either using, using the the web service like Curl or you could also do it through a website using CloudKit js. So web authentication token we talked about how you can either do the post message or you can do the URL redirect. Basically you have the JavaScript on your website and there has a button, click the button, you get this nice little window here sign in and then when you sign in if you had selected post message, you'll get the web authentication token and the data of the event in JavaScript or you will get the web authentication token as a URL in the callback URL here. Does that make sense? + +Speaker B: Yep. + +Speaker A: Yeah. In some cases if you scour the Internet so Stack overflow will tell you and this has happened to me sometimes it will not be CK web authentication token, sometimes it'll be CK session because that's what Apple likes to do. But it's the same thing. So you basically want to look for either property or query parameter name and you should be good to go and then you'll have that user as well authentication token you could do. What I, what I've been doing is, is I've been take like making a call to a like local server for instance and then essentially then I could do whatever I want with that web authentication token. As long as you have the web authentication token and the API token you can do anything on a private database that the user has rights to. So you can go, you can go to town with that all this stuff gets Swift in a cookie too. So that way it'll work. When you go back, if you have checked the box for allow, it's either a box or JavaScript method property that will say, hey, I want this to persist. It'll be Swift in a, in a cookie as well. So if you want to spelunk your cookies, you can see the web authentication token there. So that's actually the easier of the two. So that gives you the private database for the public database is where you're going to need a server to server authentication. And so to do that it's really actually not as bad as I thought it was going to be. But you go to the new server to server key, put in a name you want, it'll actually give you the command you need to run and then you just paste in the public key in here. That gives you. That will give you everything you need. So here's how to run it. Basically, sorry about that. We just run that. That gives us the key. We can go ahead and get the public key. We can also pipe it to PB Copy and then all we have to do is paste that in the box over here. There we go. It's pretty complicated to use the server key. We can spell on the miskit code on how to do it because it does a lot of that work for you if you have it. But you will need the, the private key, the key id, I think, I think that's it. And then you should be good with having access now to the public database. So just to go over, there's differences between the public and private database. So this is query. You can see my cursor, right? Query and lookup of records is available on all but file changes or, excuse me, record changes. It's not available on public zones, aren't really available in public zone changes aren't available in public notifications. Zone notifications aren't available in public, but query notifications are. And you can also do any stuff with assets which are basically binary files. You can also do that in all of them. You can't do query notifications on shared. Shared would essentially work like private essentially. So it's just a matter of who. Who's the owner and how is it shared. So one of the big challenges I think we've all faced this when we've dealt with certain web services is field type polymorphism. If you've done JSON where you don't know what type you're getting back or what data you're getting back, this can Be a bit challenging. So if you look at the documentation in Web Services Reference, there is a, there's a page called types and dictionaries and there is types. There's different type values for each field. If you're familiar with CloudKit, you've seen this, right? So you have an asset which is basically a, a binary file. You have bytes which is essentially a 60 byte base 64 encoded string, date type which is returned as a number. Double is returned as a number because These are the JavaScript types. Int is returned as a number and then there's location reference and then string and list. And how would you like, how do you do adjacent object like this? How would you even represent this in Swift? Because you don't know what type you're going to get. So like I said, this is a work in progress. Sorry. So what I do, I don't know how much you can see this. I'm going to actually move over to my documentation here at this point. So how are we doing on time? We good? + +Speaker B: Yeah, I think, I think we're doing good. + +Speaker A: Okay, cool. Any, do you want to ask questions? + +Speaker B: I don't have anything right now. + +Speaker C: Same nothing right now. But this seems applicable to things I'll be doing coming up. + +Speaker A: Okay, cool. So we have set up in the open. So we have an open API YAML file that you can pull up in Miskit, which is basically every like the documentation converted to YAML. And so what we do is you can set up in the YAML the field value requests and they have an enum type essentially for, for open API. So and then, so this has, you know, it could be one of either any of these types of. And then there's an enum in case you have a list. So if you have a list value type there is an extra property called type and then that will tell you what type the. The list is. And it's homo homomorphic. It's all the same list type. You can't have lists of different types. And then we have here again field value. Sometimes the type is available, sometimes it's not. But basically we have all the different value types available to us in a CK value. And then this is. Then the Open API generator essentially builds this for me which is. Has an enum and a struck for field field value request and then it does all the decoding for me. Thankfully I didn't have to do any of it. And then yeah, I just wanted to cover that piece where we show how we deal with these kind of like polymorphic types and how those work. The next thing I want to cover is error handling. So if you look at the documentation gives you. If you get an error we get something like this and then that will show you in the. In the table actually shows you what each error means. So again we do like an enum in YAML. It's basically a string and then we have everything else be a string. And then the open API generator will automatically generate this which gives us the server error code and the error response. It'll also do all this stuff here, which is really nice. And then we've then in our. We've abstracted a lot of this in miskit. So that way we also have now a cloud cloud error type which gives us a lot more info regarding that. So that's how we handle errors. And everything I do in the abs, the more abstract higher up stuff is done using type throws like I have type throws and everything. So that's how I handle that. Let me check one last piece I wanted to cover. The last piece I want to cover is really cool. And that is the authentication layer. So Open API provides what's called middleware and that allows you to, when you create a client or a server, you can plug that in and it will handle like let's say you need to make modifications with the request or response. When it comes in, you can intercept it and make whatever modifications you want to make. And in this case what we've done is I've created an authentication middleware which then sees if you have what's called a token manager and an authentic you have that and an authentication method. And the way it works is you pick what type of authentication you want to use. If you already have like a pre existing web token or you already have, or you, you know, have your key ID and your private key already, or you just have the API token. We've created basically a middleware that uses that. So this is how it creates the headers for server to server. So it does all this for us. And then what I added, which I think is really nice, is called the adaptive token manager. And the idea with that is like let's say you're using a client and you have the web authentication token now and then this allows you to upgrade with that web authentication token to the private database and have access to that. So and then all the, all the signing is done before you in miskit for the server to server because stuff that needs to be signed, etc. And it takes care of all that. All stuff that Claude was essentially able to decipher from the documentation. There's one more thing I wanted to show. If you want to hop in with a question while I pull something up, feel free. No questions. Cool. So I'm going to show one last thing and that is how do we actually deploy this? Is this too big, too small? Looks okay. + +Speaker C: That looks good. + +Speaker B: Yeah, it looks good. + +Speaker A: Okay, cool. So essentially what I've done is I'm using GitHub Actions. There's a way you can. This is all public by the way, so I will provide URLs in the Slack or something. Let's do this one. So this is a Swift package for Bushel. It's called Bushel Cloud. It pulls the stuff up from. Uses Miskit to go ahead and pull, get access to CloudKit and let me go back to the workflow. How familiar are you with GitHub workflows? + +Speaker C: Sadly not had the chance to work too deeply with them yet. + +Speaker A: Okay. Basically it's like for CI, but you can also set it up on a schedule. So I did that and then it runs the scheduled job and then I just execute. So then this was refactored over here into an action. There we go. And I have all sorts of stuff here for like this is generic essentially, but all these, the environment, etc. These are all passed from that workflow into here. These are basically either API keys or the information that I need for accessing Cloud, the public, public database. Right. And then I already pre built the binary. So we already have that. We're running this on Ubuntu because it's the default. Look at it. If there is no binary, it goes ahead and builds the binary for me. So that's what this is doing. And then we make sure the binary works. We make, we make it executable, we validate, make sure all the API secrets are there. We then go ahead and this validates the pim. But essentially this is the fun part. We go ahead, we have all our inputs for the private key, the key id, environment, container id. And then I use Virtual Buddy for signing verification. And. It then goes in and it runs the sync and then we'll go in. Basically it pulls from several websites information about macrosos, restore images and checks whether they're signed. And then it goes ahead and it adds those to the database. And then what this does is it exports the information in a run. Let's, let's take a look, see if I have one. I can show you. Oh, there's one scheduled. Yeah, here we go. So there's 57 new restore images created, 177 updated. 234 total. No operations failed. I also store Xcode versions and Swift versions. Those get stored as well. Had to rebuild it, but here is the results. I'm not going to pull that up, but it's essentially updated my CloudKit database and that's all in the public database. And then maybe even by the time I present this, I'll have a working example in Bushel with that example working, which would be awesome. Celestra, same idea. So this looks like it was a RSS update. We get the workflow file and. Oh, sorry, I should point out, because you're probably wondering where is all these. The stuff all these secrets stored? Yes, they are stored in Actions secrets right here. So we have our private key ID API key from Virtual Buddy. So that's all stored there. Here is Celestra. It's for updating RSS feeds. So it just basically goes through. You can look at the Swift code it goes through, pulls RSS feeds and updates them into a CloudKit record or what do you call it? Yeah, record type. And I of course try to do it in such a way not to hammer people, but same idea, yeah, it goes ahead and it runs the binary it updates and then I also have like actual parameters that I take to to filter out, like which RSS feeds are high priority and which ones aren't based on the audience and etc. So yeah, so that's deployment. That's how you can get that working. There's weird stuff with cloud with GitHub that I've noticed. If you haven't updated it in a while, it doesn't run these cron jobs. So I need to figure out a how to get around it or find another service to do it. This is all free because it's public and it is running on Ubuntu. So that's really great. And the storage on CloudKit is dirt cheap, which is even more awesome. Sorry, let's see what else. I just want to make sure I covered all my slides. The last thing I'm going to talk about is just what are my plans? Excuse me. So I don't know if you check. Follow me. But I just released. I just released Alpha 5 that has lookup zones, fetch, record changes and upload assets. Upload the assets is pretty awesome. When I saw that work because I was like, cool, I can actually upload a binary to CloudKit, which is awesome. We got query filters to work for in and not in, so you could do that I have plans to continue working on this because I think there's a big future for something like this for a lot of people. Yes, you can technically use this in Android or Windows because the Swift thing does compile in Android and Windows. You can see I already added support for that. This is the support I recently had. And then we're. I'm just kind of like going through each of these because as great as AI is, it's not perfect. So we're just kind of going through these piece by piece with each version and hammering these away and then this is actually done. I don't even know why that's there. But yeah, I think system field integration might already be there and there's a few other things. Eventually I'd like to add support. So there, there's a whole API for CloudKit schema management that I could. That would be awesome if I could figure out how to do that. If I could figure out how to do key path query filtering, that would be fantastic. And yeah, but there's a. I mean the basics is there as far as if you want to do anything with a record, it's pretty much there. One thing with Celestra is I'd love to be able to do like test out subscriptions and see how that works. So yeah, that's really the bulk of my presentation today. Now is. Now it's time to ask me a ton of questions and make me feel dumb. Go for it. + +Speaker B: No, there's a lot there to. To absorb. But I, I like the concept and I know you've been working on this for a while and I always thought it was a pretty cool, pretty cool idea and implementation of this. + +Speaker A: Questions? + +Speaker C: So with something like. Accessing CloudKit through the web, is this setup more ideal for having your server do the authentication to CloudKit with Miskit or is miskit something that you could put into even like a client side, you know, like non Swift application or I guess not non Swift but like non like app application. I'm thinking in the context of like + +Speaker A: a. + +Speaker C: I guess if I wanted to create a something accessing CloudKit that is not your typical Mac or iOS app. + +Speaker A: Can you be more specific? + +Speaker C: I'm looking into one. One approach would be browser extensions. + +Speaker A: So for like a non Safari browser. + +Speaker C: Yes. + +Speaker A: Yeah, this would be great. So basically the way you'd want that to work, like the sticky part to me would be getting the web authentication token. Other than that, like have at it. So I'm gonna, I'm gonna be devil's advocate. Why not just use the CloudKit JavaScript library. + +Speaker C: If it's an extension, my brain jumps to Swift first. + +Speaker A: Right. But it's the reason I'm asking that is like it's a, it's already a web extension. I would assume that is true. That it's 90 web based or JavaScript based. So that's where I'm just like, well, you may as well. Like, I would love. I don't want to. Like, I love tooting my own horn. Right. But like, like why not just. Unless you're. Unless you're like building a executable, I guess, or an app. Ish. + +Speaker C: And I guess another application for this would be doing CloudKit stuff server side and then providing my own API layer over it. + +Speaker A: Yep, yep. So that's. Yeah. Are we talking private database or public database? + +Speaker C: Private. + +Speaker A: So in that case, basically like you'd have to go the Hard Twitch route and you would have to provide a way to get their web authentication token, essentially, if that makes sense. And then store it in Postgres or whatever the hell you want to do. Like that's, that's the way I did it with Hard Twitch. But once you have that, you can do anything you want on the server with their private database, if that makes sense. + +Speaker C: It does. + +Speaker A: Yep. Yep. A couple of things I wanted to bring up, so let's take a look. So part of my other presentation is working, talking about cross platform automation type stuff. And the one issue I've run into is. So it basically builds on everything. Right now. I'm going to share something. + +Speaker B: Hey guys, I got to drop. But it was good presentation, Leo. Thank you. + +Speaker A: Yeah, yeah. If I have more questions, if you have any feedback, just hit me up on Slack. + +Speaker B: Sounds good. + +Speaker A: Cool, thank you. Thank you so much for helping me set this up. Yeah, talk to you later. + +Speaker B: Thank you. Bye bye. + +Speaker C: Yeah, so if you had something else to show, I'm happy to look for. I'm here for a few more minutes as well. + +Speaker A: Yeah, yeah, yeah. So I have the workflow working here and it does Ubuntu, it does Windows, it does Android. So all that stuff is available to you. I would never recommend using Miskit on an Apple platform for obvious reasons, like what's the point? + +Speaker C: True. + +Speaker A: Unless there's something special that I provide that CloudKit doesn't like, I don't get it. + +Speaker C: Right. + +Speaker A: But we have an issue. So I just started dabbling. I haven't really done anything with wasm, but I did definitely try. Like I added support for WASM in my, in my Swift build action. The thing about WASA is it does not provide. It doesn't have a transport available. So we talked about transports, I think. Did you hear about that part about the Open API generator and transports? + +Speaker C: I think I was coming in at that point. + +Speaker A: Okay. When you create a client, so underneath the client you have what's called a client transport. This is so underneath this client, this is an abstraction layer above. So this is not the right one. Where's the public one? But anyway, there is here CloudKit service maybe. Yeah, here we go. So the CloudKit service has a client and part of the client is being able to say what transport you use in Open API. And there's two transports available right now. One is, one is your regular URL session for clients, which. That makes sense. Right. And then there's the Async HTTP client which is typically used like Swift NEO based for servers. The thing is that neither of those are available in wasp. Do you know what WASM is? + +Speaker C: I have no experience with it, but yes. + +Speaker A: Okay. It's. It's the web browser. Right. So. So you really can't use Miskit in. In the. In WASM yet because there is no transport. Now having said that, why on earth would you use. Awesome. Why would you use Miskit in the browser? Why not just use CloudKit js? So that's essentially, you know, What other questions do you have? + +Speaker C: My brain is mushy right now, so + +Speaker A: because of my presentation or because other + +Speaker C: things, I got two hours of sleep. + +Speaker A: Oh, I'm so sorry. + +Speaker C: So I'm following as best as I can. + +Speaker A: Snuggling. Yeah, the intro was basically how I originally built it for hard Twitch in 2020 for a private database login for the Apple Watch because I don't want to have a login screen. And so basically there's a way in the web browser to link your Apple Watch to your account and then from there you don't need to authenticate anymore. Nice. I built that all from hand and then in 23 they came out with the Open API generator which was like, oh wait, what if I can create an open API file out of Apple's 10 year old documentation? That'd be a lot of work, but I could do it. And I don't know if you heard, but there was this thing that came out a couple years ago called AI and it's really good at creating documentation for your code, but it's also really good at creating code for your documentation. And so I was like, oh yeah, this is great. Like I can just, I can just Feed it the documentation and go from there. And, like, basically, I've been going step by step through. Like I said, if you looked at the miskit repo, like, I'm going through step by step and adding new APIs based on what's available in the documentation, piece by piece. And I would say at this point, it's like most of the really, like 80% of that people use is there. There's like, stuff like subscriptions and zones that I'm still trying to figure out, but it's. It's pretty close to done at this point. + +Speaker B: Mm. + +Speaker A: If you use it. + +Speaker C: Yeah, it's one of those. + +Speaker A: Because I. Go ahead. + +Speaker C: Yeah. I was gonna say it's one of those projects that makes me want to set up a. Like a vapor server or something just to do some Swift on the server. + +Speaker A: Yeah. Or just like, I wonder if there's like, something you do on a pie, like just hook it up to a CloudKit database. Like, there's a lot you could do here because all you need is decent os. I don't know anything about sharing. I haven't done anything with sharing yet, so I still have to do that and a few other things, but. No, yeah, + +Speaker C: it's an interesting idea. + +Speaker A: Thank you. + +Speaker B: Yeah. + +Speaker A: Well, thank you for joining, Josh. + +Speaker C: Yeah. Thanks for hosting this and sharing this info. It's nice. + +Speaker A: Yeah. If you ever run into anything, let me know. Will do. All right, talk to you later. All right, sounds good. + +Speaker C: See you. + +Speaker A: Bye. + +Speaker C: Bye. \ No newline at end of file diff --git a/docs/transcriptions/transcript.vtt b/docs/transcriptions/transcript.vtt new file mode 100644 index 00000000..0ac16ceb --- /dev/null +++ b/docs/transcriptions/transcript.vtt @@ -0,0 +1,2023 @@ +WEBVTT + +04:22.980 --> 04:25.700 +Hey, Evan, can you hear me all right? Yeah, I can hear you. + +04:26.420 --> 04:28.740 +Awesome. How do I sound? Good. + +04:30.260 --> 04:33.580 +I've used this microphone in ages. It's like + +04:34.080 --> 04:34.420 +all dusty. + +04:41.140 --> 04:44.100 +How you think I should wait like five minutes for people to come in or. + +04:44.260 --> 04:47.530 +Probably. Yeah, that there's if. Yeah, + +04:48.010 --> 04:51.930 +otherwise you can just. You could start, but that'll be interesting. + +04:52.430 --> 04:54.570 +Do you mind if I grab a cup of coffee real quick? No, not at + +04:55.070 --> 04:58.930 +all. Not at all. Okay, cool. I'm not using the AirPods mic, + +04:59.430 --> 05:02.250 +so I can hear you, but you won't be able to hear me. Okay. + +06:02.440 --> 06:27.820 +It's. + +08:51.699 --> 08:55.060 +Thank you for your patience. + +09:09.010 --> 09:12.130 +So is it just you? It looks like it's + +09:12.630 --> 09:15.970 +just me. Josh is trying to get in, but he's trying to get on on + +09:16.470 --> 09:19.250 +his mobile device and I don't think that's possible with Riverside. + +09:23.250 --> 09:26.130 +Surprised? I mean, I know they have an app. + +09:27.590 --> 09:30.070 +Maybe he's using. I'm not sure if he's using. Using the app or not. + +09:35.190 --> 09:36.310 +Should I just go? + +09:38.230 --> 09:40.470 +Sure. Okay. + +09:42.390 --> 09:45.270 +Well, thanks for joining me, Evan. I really appreciate it. + +09:47.430 --> 09:49.910 +I would say no. I mean I do, seriously. + +09:51.830 --> 09:55.470 +So yeah, this is a kind of a dry run. I would say I'm about + +09:55.970 --> 10:00.990 +60% done with this presentation about CloudKit + +10:01.490 --> 10:05.470 +on the server and we'll probably + +10:05.970 --> 10:09.990 +hop back and forth between Keynote and not Keynote, but yeah. + +10:11.670 --> 10:14.870 +So this is CloudKit as your backend from iOS + +10:15.030 --> 10:16.630 +to server side Swift. + +10:27.600 --> 10:31.200 +So what is CloudKit? CloudKit is a service + +10:32.240 --> 10:36.279 +launched by Apple probably a decade ago to + +10:36.779 --> 10:40.200 +kind of give developers a + +10:40.700 --> 10:43.680 +built in back end for storing data for their apps. + +10:44.480 --> 10:47.890 +One of the biggest benefits is is how cheap it is + +10:47.970 --> 10:49.970 +to use for iOS developers. + +10:52.450 --> 10:55.690 +So if you have built + +10:56.190 --> 10:59.970 +an app, you could just add CloudKit right here within + +11:01.330 --> 11:05.690 +the Xcode project and use + +11:06.190 --> 11:09.530 +the regular CloudKit API in Swift to go ahead and + +11:10.030 --> 11:10.850 +start using it in your app. + +11:13.390 --> 11:16.990 +Here is what it looks like to create a new record type. + +11:17.490 --> 11:20.190 +You can do all this through the CloudKit dashboard. + +11:24.190 --> 11:27.430 +In CloudKit you could also do this using a + +11:27.930 --> 11:31.710 +schema file too. And you can export and import your schema + +11:32.210 --> 11:36.030 +that way. And it's not a SQL based database, + +11:36.530 --> 11:39.910 +it's much more, no sequel ish or an abstract layer + +11:40.410 --> 11:44.120 +above it. But essentially you can create records + +11:44.520 --> 11:48.200 +kind of like a table but not quite in your records. + +11:49.400 --> 11:52.680 +You can create a struct for it. + +11:53.180 --> 11:57.039 +You can just use CloudKit directly to go ahead and then + +11:57.539 --> 12:00.520 +you can then plug it into your app and do fun stuff like this. + +12:01.560 --> 12:05.280 +We can do things like queries and basic + +12:05.780 --> 12:09.760 +database stuff. There's a lot of advantages to it. For one, + +12:10.080 --> 12:12.640 +if you're doing Apple only, + +12:13.600 --> 12:16.800 +then it definitely makes sense to look into, at least + +12:17.300 --> 12:18.080 +look into CloudKit. + +12:22.320 --> 12:25.440 +If you're just going to deploy to Apple Devices. + +12:26.080 --> 12:28.720 +If you don't mind the, + +12:29.920 --> 12:32.640 +the fact that it's not a regular SQL database, + +12:34.050 --> 12:37.050 +that's something too to think about. If you like need a SQL database, this might + +12:37.550 --> 12:40.770 +not be what you want. And then if you don't mind working + +12:41.270 --> 12:44.610 +with a lot of the abstraction layers that CloudKit provides, + +12:46.930 --> 12:50.730 +then this might be good for you to get started or especially + +12:51.230 --> 12:52.450 +if you don't have any database experience. + +12:54.130 --> 12:57.970 +So as far as like server choices, I would say CloudKit + +12:58.470 --> 13:01.970 +might not be your first choice, but it certainly is a decent choice + +13:02.290 --> 13:04.450 +if you're going the Apple only route. + +13:09.970 --> 13:13.050 +But then the question comes in, why would you want Cloud server side + +13:13.550 --> 13:16.610 +CloudKit? Why would you want to do anything with CloudKit on the server? + +13:17.970 --> 13:20.290 +So here's, here's the first case. + +13:20.690 --> 13:24.330 +Well, this is how you can go ahead and do that is they + +13:24.830 --> 13:27.880 +provide actually a REST API for calls to CloudKit + +13:28.910 --> 13:32.830 +using the, if you go to the documentation, I'll provide a link to that + +13:32.910 --> 13:36.990 +CloudKit Web Services which provides + +13:37.490 --> 13:41.270 +a lot of the documentation for what we'll be talking about today. A lot + +13:41.770 --> 13:44.790 +of this is abstracted out in the JavaScript library. So if you want to do + +13:45.290 --> 13:49.390 +stuff on a website, they provide a CloudKit JavaScript + +13:50.270 --> 13:53.710 +library for that. Sorry, + +13:56.190 --> 13:59.230 +just going into do not disturb mode. + +14:07.950 --> 14:11.710 +They even in that web references documentation they provide a + +14:12.210 --> 14:15.670 +composing web service request and all these instructions about how to go ahead and + +14:16.170 --> 14:20.110 +do that. So man, was it like half a decade ago + +14:20.880 --> 14:24.880 +that I built Heart Twitch and + +14:25.360 --> 14:28.080 +at the time I don't think there was anything, + +14:30.080 --> 14:33.840 +there was anything like sign in with Apple even. And like + +14:34.340 --> 14:38.480 +I really didn't want like to + +14:38.980 --> 14:42.600 +explain how harshwitch works is you have like a watch and it will send + +14:43.100 --> 14:47.180 +the heart rate to the server and then the + +14:47.680 --> 14:51.100 +server will then use a web socket to push it out to a web page. + +14:52.060 --> 14:55.260 +And then you would point OBS or some sort of + +14:55.760 --> 14:58.860 +streaming software to the URL or to the browser window and then that way you + +14:59.360 --> 15:02.900 +can stream your heart rate. That's how it works. And what I really didn't want + +15:03.400 --> 15:07.500 +is a difficult way for a user to log in with a username + +15:08.000 --> 15:11.260 +and password on the watch because we all know typing on the watch is hell. + +15:11.900 --> 15:15.600 +So my, my thought was like, and I didn't have sign + +15:16.100 --> 15:19.680 +in with Apple, right? So my thought was why don't we use CloudKit? + +15:19.840 --> 15:23.120 +Because you're already signed in a CloudKit on the Watch with + +15:23.620 --> 15:27.080 +your, your id. And what + +15:27.580 --> 15:31.520 +you do is you log in with a regular like email address + +15:32.020 --> 15:34.960 +and password in Heart Twitch on the website. + +15:35.840 --> 15:38.920 +And then there's a little, there's a site, there's a part of the site where + +15:39.420 --> 15:42.740 +you can sign into CloudKit and then from + +15:43.240 --> 15:46.260 +there you can, because, + +15:46.760 --> 15:52.580 +because of the CloudKit JavaScript library, you can then I can then pull the all + +15:53.080 --> 15:55.740 +the devices because when you first launch the app on the Watch, it adds your + +15:56.240 --> 15:59.540 +watch to the CloudKit database. And then I could pull that in + +16:00.040 --> 16:03.380 +and then add that to my postgres database. So then there is no need for + +16:03.880 --> 16:06.740 +authentication because I already have the CloudKit, + +16:07.720 --> 16:11.120 +the device added in my postgres database. So it's kind of like + +16:11.620 --> 16:15.520 +knows, oh yeah, this is Leo's watch, he doesn't need to authenticate. + +16:16.020 --> 16:19.440 +And that way we can link devices to accounts without having to do any + +16:19.940 --> 16:23.320 +sort of login process. And so this was my use case for + +16:23.800 --> 16:27.720 +doing server side. Essentially CloudKit + +16:28.220 --> 16:33.610 +was I could call the CloudKit web server based + +16:34.110 --> 16:37.730 +on that person's web authentication token, which we'll get all + +16:38.230 --> 16:40.370 +into later. I then pull that information in. + +16:42.050 --> 16:42.450 +So. + +16:47.250 --> 16:47.730 +Cool. + +16:50.770 --> 16:55.050 +Just checking if anybody's having issues. It doesn't look like it. So that's + +16:55.550 --> 16:59.090 +good to know. So that was the private database piece, + +16:59.950 --> 17:03.510 +but I actually think a much more useful case would be the + +17:04.010 --> 17:07.550 +public database because the idea + +17:08.050 --> 17:11.950 +would be is that you'd have some sort of app that would use central + +17:12.450 --> 17:15.950 +repository of data that it + +17:16.450 --> 17:19.710 +can pull information from. And I'm looking at both of these with + +17:19.950 --> 17:24.510 +Bushel and then an RSS reader I'm building called Celestra with + +17:25.010 --> 17:28.479 +Bushel. The. The way it's built right now is I have + +17:28.979 --> 17:32.639 +this concept of hubs and you can plug in a URL + +17:33.139 --> 17:36.999 +and that URL would provide or some sort of service. That service + +17:37.499 --> 17:41.479 +would then provide the Entire List of macOS restore images that + +17:41.979 --> 17:45.319 +are available. But then I realized like + +17:45.819 --> 17:48.919 +really there's only one location for those and each service is just going + +17:49.419 --> 17:52.850 +to be using the same URLs anyway. So if I had one + +17:53.350 --> 17:57.170 +central repository or one central database because + +17:57.670 --> 18:01.690 +they all pull from Apple, I can then parse the web for those + +18:02.190 --> 18:06.010 +restore images and then store them in CloudKit and then that way Bushel + +18:06.510 --> 18:09.970 +can then pull those from one single repository. + +18:10.210 --> 18:13.850 +And all I would have to do, and what I'm doing now is running basically + +18:14.350 --> 18:17.450 +a GitHub action or you could do like a Cron job where it would run + +18:17.950 --> 18:21.290 +on Ubuntu, wouldn't even need a Mac and it would download and scrape the + +18:21.790 --> 18:24.430 +web for restore images and storm in the public database. + +18:26.350 --> 18:29.870 +It's the same idea with Celestra. It's an RSS reader. What if I took + +18:30.370 --> 18:33.950 +those RSS RSS files in the + +18:34.450 --> 18:38.430 +web and just scrape them and then store them in a CloudKit database in + +18:38.930 --> 18:42.110 +a public database and then that way people can pull that up all through + +18:42.610 --> 18:46.550 +CloudKit. So the idea today + +18:47.050 --> 18:50.550 +is we're going to talk about how to set something, how I set something like + +18:51.050 --> 18:54.460 +this up and how you could use use my library to + +18:54.960 --> 18:57.860 +then go ahead and do this yourself for any sort of work that you're going + +18:58.360 --> 19:01.500 +to do that where you want to use either a public or private database in + +19:02.000 --> 19:05.060 +CloudKit. So this is where I introduce myself. + +19:05.940 --> 19:09.300 +So I'm going to talk today about building Miskit, which is my library + +19:09.800 --> 19:13.180 +I built for doing CloudKit stuff on the + +19:13.680 --> 19:17.140 +server or essentially off of, not off of Apple platforms. + +19:19.770 --> 19:23.130 +Evan, do you have any questions before I keep going? No, + +19:23.370 --> 19:24.890 +it's good. Good topic though. + +19:26.810 --> 19:31.090 +So like I said, we have CloudKit Web Services and CloudKit + +19:31.590 --> 19:35.330 +Web Services. We provide a lot of documentation. We talked about CloudKit + +19:35.830 --> 19:39.570 +JS and the instructions on how to compose a web service request + +19:40.070 --> 19:43.730 +which has everything I need to compose one. And back in 2020 I did + +19:44.230 --> 19:47.240 +this all manually. The thing is + +19:47.740 --> 19:50.040 +at this point, if you look at right there, + +19:51.000 --> 19:53.800 +actually if you look at the top, you can see it hasn't been updated in + +19:54.300 --> 19:58.120 +over 10 years, which is kind of crazy, + +19:58.920 --> 20:03.240 +but it works. And then we got + +20:04.200 --> 20:07.400 +introduced to something back in WWDC I want to say it was + +20:07.480 --> 20:11.360 +23. We got introduced + +20:11.860 --> 20:16.360 +to the Open API generator which is really nice because then + +20:16.840 --> 20:20.080 +we have, we can generate the Swift code if we know what the + +20:20.580 --> 20:24.080 +Open API documentation looks like it. And of course Apple doesn't provide + +20:24.580 --> 20:29.639 +one for CloudKit but they did provide a pretty big piece open. + +20:29.800 --> 20:33.320 +If you ever you looked at the Open API generator, it's amazing. Takes the + +20:33.820 --> 20:37.560 +Open API gamble file and generates all the Swift code you need. + +20:37.880 --> 20:42.160 +One of the other issues I had with first developing Miskit + +20:42.660 --> 20:46.160 +in 2020 was that there was no way to like there + +20:46.660 --> 20:50.320 +was no abstraction layer which could differentiate between doing something on the server + +20:50.720 --> 20:54.040 +or using regular like URL session + +20:54.540 --> 20:56.080 +which is more targeted towards client side. + +20:58.960 --> 21:02.800 +So I had to build my own abstraction for that. Luckily Open API has, + +21:04.080 --> 21:07.720 +there's open API transport I believe, which provides an + +21:08.220 --> 21:12.100 +abstraction layer where you can then plug in either use Async HTTP + +21:12.600 --> 21:15.660 +client, which is the server way of doing it, or you can plug in a + +21:16.160 --> 21:19.580 +URL session transport, which is of course the client way + +21:20.080 --> 21:23.740 +to do, provides a really great tutorial. + +21:24.240 --> 21:27.740 +I highly recommend checking this out as well as the + +21:28.240 --> 21:30.020 +doxy documentation that they provide. + +21:31.860 --> 21:35.180 +So this is great. But then I'd have to go ahead and I'd + +21:35.680 --> 21:39.700 +have to figure out a way to convert all this documentation into an open + +21:40.200 --> 21:44.260 +API document. I mean, can you guess what + +21:44.760 --> 21:48.260 +helped me to get build an open API document + +21:48.760 --> 21:51.620 +from all this documentation? Some of the tools, + +21:52.659 --> 21:54.980 +some AI tool. Yes. + +21:56.820 --> 21:58.980 +AI came and I'm like, holy crap. + +21:59.460 --> 22:03.060 +Like AI is really good at documenting your code, but it's also pretty + +22:03.560 --> 22:06.250 +darn good at taking documentation and building code. + +22:06.890 --> 22:10.650 +So then I would just plug it. I've been plugging in with Claude + +22:11.050 --> 22:14.730 +and it has a copy of all the documentation in my repo and + +22:15.230 --> 22:18.810 +it can go ahead and edit the open API. It's not perfect by any means, + +22:19.310 --> 22:21.610 +of course, but that's what unit tests are for. + +22:23.850 --> 22:28.090 +And actually having integration tests in order to do stuff so + +22:31.460 --> 22:31.700 +that. + +22:35.380 --> 22:41.100 +Sorry, I just want to make sure nothing + +22:46.900 --> 22:48.020 +I hate teams. + +22:53.060 --> 22:56.420 +Okay, so great. So let's talk about. + +22:59.700 --> 23:05.380 +Sorry, slides are still not done, but let's talk about authentication + +23:05.880 --> 23:09.540 +methods. You can see I have the logos here, but I haven't quite cleaned this + +23:10.040 --> 23:14.140 +up. So there's really two + +23:14.640 --> 23:17.380 +and a half authentication methods when it comes to CloudKit. + +23:18.420 --> 23:21.950 +So here is the miss demo + +23:22.450 --> 23:26.070 +database. You just go in here and you can go to tokens and keys + +23:26.570 --> 23:30.550 +and then that will give you access to set up either the API + +23:31.050 --> 23:34.550 +if you want to do API key or API token if + +23:35.050 --> 23:38.750 +you want to do a private database or a server to server keyset if you + +23:39.250 --> 23:41.950 +want to do a public database. So let's talk about the API token. + +23:42.510 --> 23:45.870 +Pretty simple. You just go into here, click the plus sign, + +23:46.840 --> 23:50.240 +you say a name and you say whether you want to do a post + +23:50.740 --> 23:54.200 +message or URL redirect. We'll get into that in a little bit in the next + +23:54.700 --> 23:58.760 +section. And then whether you want to have user info + +23:58.840 --> 24:02.960 +and you click save and you'll get a nice little API token + +24:03.460 --> 24:06.680 +you could use in your web your web calls essentially. + +24:09.000 --> 24:12.260 +API doesn't really. The API token doesn't really give you a lot of. + +24:12.570 --> 24:15.330 +But what it does give you is it gives you an entry to get a + +24:15.830 --> 24:19.450 +web authentication token for a user. So basically the way that + +24:19.950 --> 24:22.490 +works. So you'll notice here, + +24:23.050 --> 24:24.890 +when we were in this section, + +24:27.050 --> 24:30.650 +we have this piece here called Sign in Callback. So you + +24:31.150 --> 24:34.530 +can have either call a JavaScript, it's called a message + +24:35.030 --> 24:38.730 +event, it will call a Message event and a message event will have the + +24:39.230 --> 24:42.650 +metadata with the web authentication token of that user. Or you + +24:43.150 --> 24:46.730 +could do URL redirect where on authentication the user + +24:46.970 --> 24:50.930 +has a URL and then part of that URL is then having part + +24:51.430 --> 24:55.010 +of one of the query parameters and we'll get into that. We'll then have the + +24:55.510 --> 24:57.050 +web authentication token in the URL. + +24:58.570 --> 25:02.130 +So you put, basically you have your website, you add + +25:02.630 --> 25:05.970 +the JavaScript, you need to add the sign in + +25:06.470 --> 25:08.010 +with Apple. Oh, here's Josh. + +25:14.310 --> 25:15.910 +Oh cool. Josh, you there? + +25:18.790 --> 25:21.590 +I hope so. Good. Okay. + +25:21.750 --> 25:24.429 +Hey, we were just talking about how to set up. I'm going to go back + +25:24.929 --> 25:27.910 +a little bit Evan, but not too far back. Yeah, no worries. + +25:27.990 --> 25:31.270 +That's okay. But we talked about + +25:31.770 --> 25:34.310 +setting up API token and how to do that. + +25:35.910 --> 25:39.110 +So you go in here, you just click plus, + +25:39.610 --> 25:43.150 +you select your sign in callback and you put in a name and it'll + +25:43.650 --> 25:46.310 +give you an API token once you click save. Basically. + +25:50.549 --> 25:51.190 +Come on. + +25:54.470 --> 25:58.830 +The reason you want an API token is this allows you to then have + +25:59.330 --> 26:03.060 +users Sign in to CloudKit either + +26:03.560 --> 26:06.700 +using, using the the + +26:07.200 --> 26:10.860 +web service like Curl or you could also do it through a + +26:11.360 --> 26:15.500 +website using CloudKit js. So web authentication + +26:16.000 --> 26:19.140 +token we talked about how you can either do the post message or you + +26:19.640 --> 26:23.020 +can do the URL redirect. Basically you have the JavaScript + +26:23.520 --> 26:27.020 +on your website and there has a button, click the button, you get this + +26:27.520 --> 26:31.140 +nice little window here sign in and + +26:31.640 --> 26:35.020 +then when you sign in if you had selected post message, + +26:35.340 --> 26:38.500 +you'll get the web authentication token and the data of + +26:39.000 --> 26:42.460 +the event in JavaScript or you will get the web authentication + +26:42.960 --> 26:46.140 +token as a URL in the callback URL here. + +26:46.780 --> 26:47.820 +Does that make sense? + +26:50.860 --> 26:54.220 +Yep. Yeah. In some cases + +26:54.380 --> 26:58.000 +if you scour the Internet so Stack overflow will tell you and this + +26:58.500 --> 27:02.360 +has happened to me sometimes it will not be CK web authentication token, + +27:02.860 --> 27:06.040 +sometimes it'll be CK session because that's what Apple + +27:06.540 --> 27:10.120 +likes to do. But it's the same thing. + +27:10.200 --> 27:13.840 +So you basically want to look for either property or query parameter + +27:14.340 --> 27:17.520 +name and you should be good to go and then you'll have that user as + +27:18.020 --> 27:20.680 +well authentication token you could do. + +27:20.920 --> 27:23.730 +What I, what I've been doing is, + +27:25.170 --> 27:28.690 +is I've been take like making a call + +27:29.190 --> 27:32.690 +to a like local server for instance and then essentially + +27:33.410 --> 27:36.690 +then I could do whatever I want with that web authentication token. As long as + +27:37.190 --> 27:40.730 +you have the web authentication token and the API token you can do anything on + +27:41.230 --> 27:44.050 +a private database that the user has rights to. + +27:44.450 --> 27:47.610 +So you can go, you can go to town with + +27:48.110 --> 27:51.420 +that all this stuff gets Swift in a cookie too. + +27:51.580 --> 27:55.260 +So that way it'll work. When you go back, if you + +27:55.500 --> 27:57.500 +have checked the box for allow, + +27:58.780 --> 28:01.940 +it's either a box or JavaScript method property that will say, + +28:02.440 --> 28:05.179 +hey, I want this to persist. It'll be Swift in a, in a cookie as + +28:05.679 --> 28:09.340 +well. So if you want to spelunk your cookies, you can see the web authentication + +28:09.840 --> 28:13.180 +token there. So that's actually the easier of the + +28:13.680 --> 28:16.940 +two. So that gives you the private database for the public database + +28:17.440 --> 28:19.820 +is where you're going to need a server to server authentication. + +28:21.340 --> 28:24.620 +And so to do that it's really actually not as + +28:25.120 --> 28:27.980 +bad as I thought it was going to be. But you go to the new + +28:28.220 --> 28:32.020 +server to server key, put in a name you want, it'll actually give you + +28:32.520 --> 28:35.180 +the command you need to run and then you just paste in the public key + +28:35.680 --> 28:37.340 +in here. That gives you. + +28:38.780 --> 28:42.300 +That will give you everything you need. So here's how to run it. + +28:42.800 --> 28:44.630 +Basically, sorry about that. + +28:57.190 --> 28:59.510 +We just run that. That gives us the key. + +29:00.710 --> 29:04.670 +We can go ahead and get the public key. We can also pipe + +29:05.170 --> 29:08.510 +it to PB Copy and then all we have to do is paste that in + +29:09.010 --> 29:10.930 +the box over here. + +29:17.970 --> 29:18.690 +There we go. + +29:25.890 --> 29:28.770 +It's pretty complicated to use the server key. + +29:30.050 --> 29:33.610 +We can spell on the miskit code on how to do it because it + +29:34.110 --> 29:37.090 +does a lot of that work for you if you have it. But you will + +29:37.590 --> 29:41.170 +need the, the private key, the key id, + +29:42.290 --> 29:46.490 +I think, I think that's it. And then you should be good with + +29:46.990 --> 29:50.130 +having access now to the public database. + +29:50.850 --> 29:54.730 +So just to go over, there's differences between the public and private + +29:55.230 --> 29:59.090 +database. So this is query. + +29:59.570 --> 30:03.010 +You can see my cursor, right? Query and lookup of records + +30:03.510 --> 30:07.110 +is available on all but file changes or, + +30:07.610 --> 30:11.390 +excuse me, record changes. It's not available on public zones, + +30:11.890 --> 30:16.470 +aren't really available in public zone changes aren't available in public notifications. + +30:16.550 --> 30:18.870 +Zone notifications aren't available in public, + +30:19.670 --> 30:23.350 +but query notifications are. And you can also do + +30:23.850 --> 30:27.470 +any stuff with assets which are basically binary files. You can also + +30:27.970 --> 30:32.190 +do that in all of them. You can't do query + +30:32.690 --> 30:36.390 +notifications on shared. Shared would essentially work like private + +30:36.850 --> 30:40.530 +essentially. So it's just a matter of who. + +30:41.030 --> 30:42.610 +Who's the owner and how is it shared. + +30:44.690 --> 30:47.810 +So one of the big challenges I think we've all faced this + +30:48.310 --> 30:52.449 +when we've dealt with certain web services is field type + +30:52.949 --> 30:56.570 +polymorphism. If you've done JSON where you don't know what type you're getting back or + +30:57.070 --> 30:59.410 +what data you're getting back, this can Be a bit challenging. + +31:00.530 --> 31:04.490 +So if you look at the documentation in + +31:04.990 --> 31:08.290 +Web Services Reference, there is a, + +31:09.090 --> 31:13.170 +there's a page called types and dictionaries and there is types. + +31:14.050 --> 31:17.890 +There's different type values for each field. If you're familiar with CloudKit, you've seen + +31:18.390 --> 31:22.610 +this, right? So you have an asset which is basically a, + +31:24.290 --> 31:28.210 +a binary file. You have bytes + +31:29.090 --> 31:33.140 +which is essentially a 60 byte base 64 encoded + +31:33.640 --> 31:36.860 +string, date type which is returned as a + +31:37.360 --> 31:40.620 +number. Double is returned as a number because These are the + +31:41.120 --> 31:44.580 +JavaScript types. Int is returned as a number + +31:45.700 --> 31:49.620 +and then there's location reference and + +31:50.120 --> 31:53.420 +then string and list. And how would you like, + +31:53.920 --> 31:57.300 +how do you do adjacent object like this? How would you even + +31:57.800 --> 31:59.860 +represent this in Swift? Because you don't know what type you're going to get. + +32:01.350 --> 32:04.510 +So like I said, this is a work in progress. + +32:05.010 --> 32:08.710 +Sorry. So what I do, I don't know how much you can see this. + +32:09.110 --> 32:13.910 +I'm going to actually move over to my documentation + +32:14.410 --> 32:18.590 +here at this point. So how + +32:19.090 --> 32:22.870 +are we doing on time? We good? Yeah, + +32:23.370 --> 32:25.910 +I think, I think we're doing good. Okay, cool. Any, + +32:26.560 --> 32:30.240 +do you want to ask questions? I don't + +32:30.740 --> 32:32.160 +have anything right now. + +32:33.760 --> 32:37.600 +Same nothing right now. But this seems applicable to things + +32:38.100 --> 32:40.480 +I'll be doing coming up. Okay, cool. + +32:43.200 --> 32:46.640 +So we have set up in the + +32:46.800 --> 32:50.240 +open. So we have an open API YAML file that you + +32:50.740 --> 32:55.370 +can pull up in Miskit, which is basically every like the + +32:55.870 --> 32:59.570 +documentation converted to YAML. And so what we do + +33:00.070 --> 33:03.410 +is you can set up in the YAML the + +33:03.910 --> 33:08.330 +field value requests and they have an enum type essentially for, + +33:12.090 --> 33:15.490 +for open API. So and then, + +33:15.990 --> 33:18.810 +so this has, you know, it could be one of either any of these types + +33:18.860 --> 33:22.730 +of. And then there's an enum in case you have + +33:23.230 --> 33:27.250 +a list. So if you have a list value + +33:27.330 --> 33:31.450 +type there is an extra property called type + +33:31.950 --> 33:36.050 +and then that will tell you what type the. The list is. And it's + +33:36.530 --> 33:40.210 +homo homomorphic. It's all the same list + +33:40.710 --> 33:42.210 +type. You can't have lists of different types. + +33:44.050 --> 33:49.230 +And then we have here again + +33:49.730 --> 33:52.750 +field value. Sometimes the type is available, + +33:52.910 --> 33:56.150 +sometimes it's not. But basically we have all + +33:56.650 --> 33:59.950 +the different value types available to us in a CK value. + +34:01.950 --> 34:05.670 +And then this is. Then the Open API + +34:06.170 --> 34:09.150 +generator essentially builds this for me which is. + +34:09.710 --> 34:13.630 +Has an enum and a struck for field field value request + +34:15.329 --> 34:18.449 +and then it does all the decoding for me. Thankfully I didn't have to + +34:18.949 --> 34:19.169 +do any of it. + +34:23.089 --> 34:26.569 +And then yeah, I just wanted to + +34:27.069 --> 34:30.209 +cover that piece where we show how we deal with these + +34:30.709 --> 34:34.289 +kind of like polymorphic types and how those work. + +34:35.329 --> 34:37.489 +The next thing I want to cover is error handling. + +34:39.249 --> 34:42.209 +So if you look at the documentation gives you. + +34:43.390 --> 34:48.350 +If you get an error we get something like this and + +34:48.850 --> 34:52.350 +then that will show you in the. In the table actually shows you what each + +34:52.830 --> 34:56.150 +error means. So again we do + +34:56.650 --> 35:00.110 +like an enum in YAML. It's basically a string + +35:00.610 --> 35:04.270 +and then we have everything else be a string. And then the open API + +35:04.770 --> 35:08.110 +generator will automatically generate this which + +35:08.610 --> 35:11.820 +gives us the server error code and the error response. + +35:12.380 --> 35:15.500 +It'll also do all this stuff here, which is really nice. + +35:17.980 --> 35:21.580 +And then we've then in our. We've abstracted a lot of + +35:22.080 --> 35:25.860 +this in miskit. So that way we also have now a + +35:26.360 --> 35:29.980 +cloud cloud error type which gives us a lot more + +35:30.060 --> 35:31.820 +info regarding that. + +35:33.900 --> 35:37.520 +So that's how we handle errors. And everything I do + +35:38.020 --> 35:42.200 +in the abs, the more abstract higher up stuff is done using + +35:42.360 --> 35:44.920 +type throws like I have type throws and everything. + +35:45.160 --> 35:47.240 +So that's how I handle that. + +35:48.600 --> 35:52.200 +Let me check one last piece I wanted to cover. + +35:54.920 --> 35:58.200 +The last piece I want to cover is really cool. And that is + +35:58.700 --> 36:01.920 +the authentication layer. So Open API provides + +36:02.420 --> 36:05.960 +what's called middleware and that allows you to, + +36:06.200 --> 36:09.120 +when you create a client or a server, you can plug that in and it + +36:09.620 --> 36:13.400 +will handle like let's say you need to make modifications with the request or response. + +36:13.640 --> 36:17.040 +When it comes in, you can intercept it and make whatever modifications + +36:17.540 --> 36:21.160 +you want to make. And in this case what + +36:21.660 --> 36:25.480 +we've done is I've created an authentication + +36:25.980 --> 36:29.800 +middleware which then sees if you have + +36:31.430 --> 36:35.310 +what's called a token manager and an authentic + +36:35.810 --> 36:39.350 +you have that and an authentication method. And the way it works + +36:39.510 --> 36:42.950 +is you pick what type of authentication you want to + +36:43.450 --> 36:46.789 +use. If you already have like a pre existing web token or you already have, + +36:47.289 --> 36:50.350 +or you, you know, have your key ID and your private key already, or you + +36:50.850 --> 36:54.470 +just have the API token. We've created basically a middleware that + +36:54.970 --> 36:59.120 +uses that. So this + +36:59.620 --> 37:03.160 +is how it creates the headers for server to server. So it + +37:03.660 --> 37:07.760 +does all this for us. And then what + +37:08.260 --> 37:11.760 +I added, which I think is really nice, is called the adaptive token manager. + +37:12.240 --> 37:17.360 +And the idea with that is like let's say you're + +37:17.860 --> 37:20.920 +using a client and you have the web authentication token + +37:21.420 --> 37:25.450 +now and then this allows you to upgrade with that web authentication token + +37:25.950 --> 37:27.730 +to the private database and have access to that. + +37:30.530 --> 37:33.970 +So and then all the, all the signing is done + +37:34.470 --> 37:38.090 +before you in miskit for the server to server because stuff that needs to be + +37:38.590 --> 37:42.170 +signed, etc. And it takes care of all that. All stuff + +37:42.670 --> 37:45.970 +that Claude was essentially able to decipher from + +37:46.610 --> 37:50.060 +the documentation. + +37:52.620 --> 37:54.300 +There's one more thing I wanted to show. + +37:56.380 --> 37:59.860 +If you want to hop in with a question while I pull something + +38:00.360 --> 38:00.940 +up, feel free. + +38:21.190 --> 38:24.390 +No questions. Cool. + +38:24.790 --> 38:28.630 +So I'm going to show one last thing and that is how + +38:28.710 --> 38:30.310 +do we actually deploy this? + +38:33.350 --> 38:36.950 +Is this too big, too small? Looks okay. + +38:37.590 --> 38:40.070 +That looks good. Yeah, it looks good. Okay, cool. + +38:43.850 --> 38:47.210 +So essentially what I've done is I'm using + +38:47.370 --> 38:50.410 +GitHub Actions. There's a way you can. + +38:53.130 --> 38:57.330 +This is all public by the way, so I will provide URLs + +38:57.830 --> 39:00.570 +in the Slack or something. Let's do this one. + +39:02.410 --> 39:07.220 +So this is a Swift package for + +39:07.720 --> 39:10.660 +Bushel. It's called Bushel Cloud. It pulls the stuff up from. + +39:11.220 --> 39:14.740 +Uses Miskit to go ahead and + +39:16.740 --> 39:20.340 +pull, get access to CloudKit and + +39:21.060 --> 39:24.860 +let me go back to the workflow. How familiar + +39:25.360 --> 39:26.580 +are you with GitHub workflows? + +39:29.860 --> 39:32.980 +Sadly not had the chance to work too deeply with them yet. + +39:33.690 --> 39:37.050 +Okay. Basically it's like for CI, but you can + +39:37.550 --> 39:41.570 +also set it up on a schedule. So I did that and + +39:42.070 --> 39:45.730 +then it runs the scheduled job and then I just + +39:46.230 --> 39:46.490 +execute. + +39:50.650 --> 39:54.650 +So then this was refactored over here into + +39:55.150 --> 39:58.490 +an action. There we go. + +39:59.540 --> 40:03.460 +And I have all sorts of stuff here for + +40:05.380 --> 40:10.300 +like this is generic essentially, but all + +40:10.800 --> 40:14.060 +these, the environment, etc. These are all passed + +40:14.560 --> 40:18.180 +from that workflow into here. These are basically either API keys or + +40:18.680 --> 40:22.100 +the information that I need for accessing Cloud, the public, + +40:24.020 --> 40:28.120 +public database. Right. And then I + +40:28.620 --> 40:32.040 +already pre built the binary. So we already + +40:32.540 --> 40:35.960 +have that. We're running this on Ubuntu because + +40:36.460 --> 40:40.280 +it's the default. Look at it. If there + +40:40.780 --> 40:44.400 +is no binary, it goes ahead and builds the binary for me. So that's + +40:44.900 --> 40:49.080 +what this is doing. And then we + +40:49.580 --> 40:53.290 +make sure the binary works. We make, we make it executable, we validate, + +40:53.790 --> 40:56.530 +make sure all the API secrets are there. + +40:57.650 --> 41:00.530 +We then go ahead and this validates the pim. + +41:00.690 --> 41:04.050 +But essentially this is the fun part. We go ahead, + +41:04.550 --> 41:07.730 +we have all our inputs for the private key, the key id, + +41:07.810 --> 41:11.050 +environment, container id. And then + +41:11.550 --> 41:14.450 +I use Virtual Buddy for signing verification. And. + +41:18.460 --> 41:21.940 +It then goes in and it runs the + +41:22.440 --> 41:25.660 +sync and then we'll go in. + +41:25.980 --> 41:29.900 +Basically it pulls from several websites information about + +41:30.400 --> 41:33.780 +macrosos, restore images and checks whether they're signed. And then + +41:34.280 --> 41:38.260 +it goes ahead and it adds those to the database. + +41:38.760 --> 41:42.100 +And then what this does is it exports the information in a run. + +41:42.600 --> 41:44.860 +Let's, let's take a look, see if I have one. I can show you. + +41:45.980 --> 41:47.420 +Oh, there's one scheduled. + +41:50.060 --> 41:54.060 +Yeah, here we go. So there's 57 new + +41:54.560 --> 41:58.300 +restore images created, 177 updated. + +41:58.780 --> 42:03.020 +234 total. No operations failed. + +42:03.100 --> 42:05.900 +I also store Xcode versions and Swift versions. + +42:06.780 --> 42:10.460 +Those get stored as well. Had to rebuild it, + +42:10.630 --> 42:11.830 +but here is the results. + +42:13.750 --> 42:17.750 +I'm not going to pull that up, but it's essentially updated + +42:18.250 --> 42:22.470 +my CloudKit database and + +42:22.550 --> 42:26.190 +that's all in the public database. And then maybe even by the time + +42:26.690 --> 42:30.230 +I present this, I'll have a working example in Bushel with that example working, + +42:30.630 --> 42:31.670 +which would be awesome. + +42:32.870 --> 42:36.630 +Celestra, same idea. So this looks like it was a RSS + +42:37.130 --> 42:42.830 +update. We get the workflow file and. + +42:43.330 --> 42:46.110 +Oh, sorry, I should point out, because you're probably wondering where is all these. + +42:46.610 --> 42:50.150 +The stuff all these secrets stored? Yes, they are stored in + +42:50.650 --> 42:53.910 +Actions secrets right here. So we have + +42:54.410 --> 42:58.190 +our private key ID API key from + +42:58.690 --> 43:02.750 +Virtual Buddy. So that's all stored there. Here is + +43:03.150 --> 43:06.350 +Celestra. It's for updating RSS feeds. + +43:07.050 --> 43:10.370 +So it just basically goes through. You can look at the Swift code it goes + +43:10.870 --> 43:15.930 +through, pulls RSS feeds and updates them into a CloudKit record + +43:16.410 --> 43:18.490 +or what do you call it? Yeah, record type. + +43:19.850 --> 43:22.210 +And I of course try to do it in such a way not to hammer + +43:22.710 --> 43:24.170 +people, but same idea, + +43:27.050 --> 43:30.610 +yeah, it goes ahead and it runs the + +43:31.110 --> 43:35.890 +binary it updates and then I also have like actual parameters + +43:36.390 --> 43:39.810 +that I take to to filter out, like which RSS feeds are high + +43:40.310 --> 43:44.330 +priority and which ones aren't based on the audience and etc. So yeah, + +43:44.890 --> 43:48.410 +so that's deployment. That's how you can get that working. + +43:48.810 --> 43:53.130 +There's weird stuff with cloud with GitHub that + +43:53.690 --> 43:57.210 +I've noticed. If you haven't updated it in a while, it doesn't run these + +43:57.710 --> 43:59.570 +cron jobs. So I need to figure out a how to get around it or + +44:00.070 --> 44:03.550 +find another service to do it. This is all free + +44:03.630 --> 44:07.310 +because it's public and it + +44:07.810 --> 44:09.870 +is running on Ubuntu. So that's really great. + +44:12.350 --> 44:16.310 +And the storage on CloudKit is dirt cheap, which is even more + +44:16.810 --> 44:16.830 +awesome. + +44:20.030 --> 44:23.990 +Sorry, let's see what else. I just + +44:24.490 --> 44:27.150 +want to make sure I covered all my slides. The last thing I'm going to + +44:27.650 --> 44:31.030 +talk about is just what are my plans? Excuse me. + +44:31.510 --> 44:34.550 +So I don't know if you check. Follow me. But I just released. + +44:41.910 --> 44:45.390 +I just released Alpha 5 that has lookup + +44:45.890 --> 44:49.270 +zones, fetch, record changes and upload assets. Upload the assets + +44:49.770 --> 44:52.470 +is pretty awesome. When I saw that work because I was like, cool, I can + +44:52.970 --> 44:56.230 +actually upload a binary to CloudKit, which is awesome. + +44:57.310 --> 45:00.550 +We got query filters to work for in and not in, so you could do + +45:01.050 --> 45:04.230 +that I have plans to continue working on this because I think + +45:04.730 --> 45:06.990 +there's a big future for something like this for a lot of people. + +45:09.150 --> 45:12.270 +Yes, you can technically use this in Android or Windows + +45:12.670 --> 45:16.230 +because the Swift thing does compile in Android and Windows. + +45:16.730 --> 45:19.790 +You can see I already added support for that. This is the support I recently + +45:19.870 --> 45:23.200 +had. And then we're. I'm just kind of like + +45:23.700 --> 45:27.000 +going through each of these because as great as AI is, it's not perfect. + +45:27.080 --> 45:30.760 +So we're just kind of going through these piece by piece + +45:30.840 --> 45:35.720 +with each version and hammering these away and + +45:36.220 --> 45:40.160 +then this is actually done. I don't even know why that's there. But yeah, + +45:40.660 --> 45:44.760 +I think system field integration might already be there and there's a few other things. + +45:45.960 --> 45:49.200 +Eventually I'd like to add support. So there, there's a + +45:49.700 --> 45:53.200 +whole API for CloudKit schema management that I could. + +45:53.700 --> 45:56.120 +That would be awesome if I could figure out how to do that. If I + +45:56.620 --> 45:59.400 +could figure out how to do key path query filtering, that would be fantastic. + +46:01.720 --> 46:05.280 +And yeah, but there's a. I mean the basics is there as + +46:05.780 --> 46:09.080 +far as if you want to do anything with a record, it's pretty much there. + +46:09.720 --> 46:13.160 +One thing with Celestra is I'd love to be able to do like test out + +46:13.660 --> 46:17.840 +subscriptions and see how that works. So yeah, + +46:18.340 --> 46:20.040 +that's really the bulk of my presentation today. + +46:21.800 --> 46:24.880 +Now is. Now it's time to ask me a ton of questions and make me + +46:25.380 --> 46:28.840 +feel dumb. Go for it. No, + +46:29.880 --> 46:33.400 +there's a lot there to. To absorb. But I, I like + +46:33.900 --> 46:36.680 +the concept and I know you've been working on this for a while and I + +46:37.180 --> 46:41.630 +always thought it was a pretty cool, pretty cool idea and implementation + +46:42.130 --> 46:43.470 +of this. Questions? + +46:48.990 --> 46:50.030 +So with something like. + +46:54.110 --> 46:57.510 +Accessing CloudKit through the web, is this + +46:58.010 --> 47:01.630 +setup more ideal for having your server + +47:01.870 --> 47:05.550 +do the authentication to CloudKit with Miskit + +47:05.970 --> 47:09.890 +or is miskit something that you could put into even like a client + +47:10.130 --> 47:15.090 +side, you know, like non + +47:15.810 --> 47:19.410 +Swift application or I guess not non Swift but like non like + +47:19.910 --> 47:22.049 +app application. I'm thinking in the context of like a. + +47:25.730 --> 47:30.290 +I guess if I wanted to create a something + +47:30.790 --> 47:33.410 +accessing CloudKit that is not your typical Mac or iOS app. + +47:34.880 --> 47:38.480 +Can you be more specific? I'm looking + +47:38.720 --> 47:42.040 +into one. One approach would be browser + +47:42.540 --> 47:46.000 +extensions. So for + +47:46.500 --> 47:48.240 +like a non Safari browser. Yes. + +47:50.400 --> 47:54.120 +Yeah, this would be great. So basically the way you'd want + +47:54.620 --> 47:58.240 +that to work, like the sticky part to me would be getting the web authentication + +47:58.740 --> 48:01.090 +token. Other than that, like have at it. + +48:04.610 --> 48:08.770 +So I'm gonna, I'm gonna be devil's advocate. Why not just use the CloudKit + +48:08.850 --> 48:11.490 +JavaScript library. If it's an extension, + +48:12.450 --> 48:16.129 +my brain jumps to Swift first. Right. + +48:16.629 --> 48:18.930 +But it's the reason I'm asking that is like it's a, + +48:19.410 --> 48:22.050 +it's already a web extension. I would assume that is true. + +48:22.690 --> 48:26.010 +That it's 90 web based or JavaScript + +48:26.510 --> 48:29.600 +based. So that's where I'm just like, well, you may as well. Like, + +48:29.840 --> 48:32.800 +I would love. I don't want to. Like, I love tooting my own horn. + +48:33.300 --> 48:37.120 +Right. But like, like why not just. Unless you're. + +48:40.720 --> 48:43.840 +Unless you're like building a executable, + +48:44.160 --> 48:45.920 +I guess, or an app. Ish. + +48:47.760 --> 48:50.960 +And I guess another application for this would be + +48:51.680 --> 48:55.920 +doing CloudKit stuff server side and then providing my own API + +48:56.420 --> 48:59.860 +layer over it. Yep, yep. So that's. + +49:00.360 --> 49:03.740 +Yeah. Are we talking private database or public database? Private. + +49:05.580 --> 49:09.380 +So in that case, basically like you'd have to go the + +49:09.880 --> 49:12.979 +Hard Twitch route and you would + +49:13.479 --> 49:16.820 +have to provide a way to get their web + +49:17.320 --> 49:19.900 +authentication token, essentially, if that makes sense. + +49:20.540 --> 49:23.260 +And then store it in Postgres or whatever the hell you want to do. + +49:23.760 --> 49:26.880 +Like that's, that's the way I did it with Hard Twitch. But once you have + +49:27.380 --> 49:31.200 +that, you can do anything you want on the server with their private database, + +49:31.700 --> 49:34.480 +if that makes sense. It does. Yep. + +49:34.560 --> 49:37.920 +Yep. A couple of things I wanted to bring + +49:38.420 --> 49:39.520 +up, so let's take a look. + +49:44.000 --> 49:48.400 +So part of my other presentation + +49:48.640 --> 49:51.880 +is working, talking about cross + +49:52.380 --> 49:54.440 +platform automation type stuff. + +49:55.560 --> 49:58.840 +And the one issue I've run into is. + +49:58.920 --> 50:01.560 +So it basically builds on everything. Right now. + +50:07.560 --> 50:11.320 +I'm going to share something. Hey guys, I got + +50:11.820 --> 50:15.240 +to drop. But it was good presentation, Leo. Thank you. Yeah, + +50:15.740 --> 50:17.760 +yeah. If I have more questions, if you have any feedback, just hit me up + +50:18.260 --> 50:21.710 +on Slack. Sounds good. Cool, thank you. Thank you so much for + +50:22.210 --> 50:25.350 +helping me set this up. Yeah, talk to you later. Thank you. + +50:25.850 --> 50:29.190 +Bye bye. Yeah, + +50:29.690 --> 50:31.790 +so if you had something else to show, I'm happy to look for. I'm here + +50:32.290 --> 50:34.390 +for a few more minutes as well. Yeah, yeah, yeah. + +50:38.790 --> 50:43.110 +So I have the workflow working here and it does Ubuntu, + +50:44.080 --> 50:48.000 +it does Windows, it does Android. So all that stuff is available to you. + +50:48.640 --> 50:52.040 +I would never recommend using Miskit on an Apple platform + +50:52.540 --> 50:56.080 +for obvious reasons, like what's the point? True. + +50:56.580 --> 50:59.920 +Unless there's something special that I provide that CloudKit doesn't like, I don't + +51:00.420 --> 51:03.840 +get it. Right. But we have an issue. + +51:03.920 --> 51:07.640 +So I just started dabbling. I haven't really done anything + +51:08.140 --> 51:11.730 +with wasm, but I did definitely try. Like I added support for + +51:12.230 --> 51:14.890 +WASM in my, in my Swift build action. + +51:17.210 --> 51:21.530 +The thing about WASA is it does not provide. It doesn't have a transport available. + +51:22.570 --> 51:24.410 +So we talked about transports, + +51:26.010 --> 51:30.090 +I think. Did you hear about that part about the Open API generator and transports? + +51:31.370 --> 51:33.690 +I think I was coming in at that point. + +51:34.410 --> 51:38.310 +Okay. When you create a client, so underneath + +51:38.810 --> 51:42.630 +the client you + +51:43.130 --> 51:46.990 +have what's called a client transport. This is so underneath this + +51:47.490 --> 51:50.829 +client, this is an abstraction layer above. So this is not + +51:51.329 --> 51:53.390 +the right one. Where's the public one? + +52:00.680 --> 52:05.440 +But anyway, there is here + +52:05.940 --> 52:06.920 +CloudKit service maybe. + +52:09.560 --> 52:13.640 +Yeah, here we go. So the CloudKit service has + +52:14.140 --> 52:17.960 +a client and part of the client is being able + +52:19.960 --> 52:23.560 +to say what transport you use in Open API. + +52:24.760 --> 52:29.330 +And there's + +52:29.830 --> 52:33.730 +two transports available right now. One is, + +52:36.850 --> 52:40.930 +one is your regular URL session for clients, which. That makes sense. + +52:41.430 --> 52:45.410 +Right. And then there's the Async HTTP client which is typically used + +52:45.570 --> 52:47.970 +like Swift NEO based for servers. + +52:49.330 --> 52:53.170 +The thing is that neither of those are available in wasp. + +52:54.290 --> 52:57.810 +Do you know what WASM is? I have no experience with it, but yes. + +52:58.850 --> 53:01.490 +Okay. It's. It's the web browser. Right. + +53:01.890 --> 53:04.850 +So. So you really can't use Miskit in. + +53:06.450 --> 53:09.650 +In the. In WASM yet because there is no transport. Now having + +53:10.150 --> 53:12.450 +said that, why on earth would you use. + +53:13.090 --> 53:16.970 +Awesome. Why would you use Miskit in the browser? Why not just use CloudKit + +53:17.470 --> 53:20.700 +js? So that's essentially, + +53:21.580 --> 53:22.060 +you know, + +53:29.260 --> 53:30.940 +What other questions do you have? + +53:35.660 --> 53:41.340 +My brain is mushy right now, so because + +53:41.840 --> 53:45.850 +of my presentation or because other things, I got two hours of sleep. + +53:46.650 --> 53:50.170 +Oh, I'm so sorry. So I'm + +53:50.670 --> 53:51.450 +following as best as I can. + +53:54.330 --> 53:58.010 +Snuggling. Yeah, the intro + +53:58.090 --> 54:01.570 +was basically how I originally built it for + +54:02.070 --> 54:06.210 +hard Twitch in 2020 for a private database login for + +54:06.710 --> 54:09.210 +the Apple Watch because I don't want to have a login screen. And so basically + +54:09.710 --> 54:12.490 +there's a way in the web browser to link your Apple Watch to your account + +54:12.990 --> 54:16.280 +and then from there you don't need to authenticate anymore. Nice. I built + +54:16.780 --> 54:20.560 +that all from hand and then in 23 they came out + +54:21.060 --> 54:24.160 +with the Open API generator which was like, oh wait, + +54:24.660 --> 54:29.040 +what if I can create an open API file out of Apple's + +54:29.280 --> 54:30.800 +10 year old documentation? + +54:33.120 --> 54:36.560 +That'd be a lot of work, but I could do it. And I don't know + +54:37.060 --> 54:40.720 +if you heard, but there was this thing that came out a couple + +54:41.220 --> 54:45.340 +years ago called AI and it's + +54:45.840 --> 54:49.140 +really good at creating documentation for your code, but it's also really good at creating + +54:49.640 --> 54:53.940 +code for your documentation. And so I was like, oh yeah, + +54:54.440 --> 54:57.739 +this is great. Like I can just, I can just Feed + +54:58.239 --> 55:01.620 +it the documentation and go from + +55:02.120 --> 55:05.140 +there. And, like, basically, I've been going step by step through. + +55:05.940 --> 55:09.300 +Like I said, if you looked at the miskit repo, + +55:09.800 --> 55:14.620 +like, I'm going through step by step and adding new APIs based + +55:15.120 --> 55:18.180 +on what's available in the documentation, piece by piece. And I would say at this + +55:18.680 --> 55:21.940 +point, it's like most of the really, like 80% of that + +55:22.440 --> 55:26.340 +people use is there. There's like, stuff like subscriptions and zones that I'm + +55:26.840 --> 55:30.260 +still trying to figure out, but it's. It's pretty close to done + +55:30.760 --> 55:31.900 +at this point. Mm. + +55:35.110 --> 55:38.590 +If you use it. Yeah, it's one of those. Because I. Go ahead. + +55:39.090 --> 55:41.070 +Yeah. I was gonna say it's one of those projects that makes me want to + +55:41.570 --> 55:45.110 +set up a. Like a vapor server or something just to do some Swift on + +55:45.610 --> 55:49.390 +the server. Yeah. Or just like, I wonder + +55:49.890 --> 55:52.990 +if there's like, something you do on a pie, like just hook it up to + +55:53.490 --> 55:56.030 +a CloudKit database. Like, there's a lot you could do here because all you need + +55:56.530 --> 56:00.430 +is decent os. I don't know anything about sharing. + +56:00.930 --> 56:03.390 +I haven't done anything with sharing yet, so I still have to do that and + +56:03.890 --> 56:05.740 +a few other things, but. No, yeah, + +56:07.740 --> 56:10.460 +it's an interesting idea. Thank you. + +56:11.420 --> 56:15.340 +Yeah. Well, thank you for joining, Josh. Yeah. Thanks for hosting this and + +56:15.900 --> 56:19.260 +sharing this info. It's nice. Yeah. If you + +56:19.760 --> 56:22.060 +ever run into anything, let me know. Will do. + +56:22.940 --> 56:25.660 +All right, talk to you later. All right, sounds good. See you. + +56:26.220 --> 56:26.700 +Bye. From b0c65d824db740dabf3b73ba155820d2e67021c3 Mon Sep 17 00:00:00 2001 From: leogdion Date: Mon, 18 May 2026 15:02:45 +0100 Subject: [PATCH 02/35] Pre-1.0.0 correctness & safety hardening (#357) --- .../DataSources/TheAppleWiki/IPSWParser.swift | 1 - .../Commands/AddFeedCommand.swift | 1 - .../CelestraCloud/Commands/ClearCommand.swift | 1 - .../Commands/UpdateCommand.swift | 4 - .../Services/FeedUpdateProcessor+Fetch.swift | 1 - .../Services/FeedUpdateProcessor.swift | 1 - .../CelestraCloudKit/CelestraConfig.swift | 2 - .../Configuration/ConfigurationLoader.swift | 1 - .../Protocols/CloudKitRecordOperating.swift | 1 - .../Services/ArticleCategorizer.swift | 1 - .../Services/ArticleCloudKitService.swift | 1 - .../Services/ArticleOperationBuilder.swift | 1 - .../Services/ArticleSyncService.swift | 1 - .../Services/CelestraError.swift | 7 + .../Services/CloudKitService+Celestra.swift | 1 - .../Services/FeedCloudKitService.swift | 1 - .../Commands/DemoErrorsRunner+Output.swift | 17 ++- .../Commands/DemoInFilterCommand.swift | 1 - .../Commands/QueryCommand+FilterParsing.swift | 4 - .../MistDemoKit/Commands/QueryCommand.swift | 33 ++-- .../MistDemoKit/Server/WebBackend.swift | 1 - .../MistKitClientFactoryTests+Helpers.swift | 8 - ...lientFactoryTests+ServerToServerAuth.swift | 10 +- ...erTests+AuthenticationMethodPriority.swift | 1 - ...erTests+ServerToServerAuthentication.swift | 5 - Package.swift | 10 +- README.md | 18 +-- .../Credentials+TokenManager.swift | 9 +- .../HashFunction+CloudKitBodyHash.swift | 1 - .../ServerToServerAuthManager.swift | 2 - .../ServerToServerAuthenticator.swift | 1 - .../CloudKitError+OpenAPI.swift | 55 ++++++- .../CloudKitService/CloudKitError.swift | 38 +++++ .../CloudKitResponseProcessor+Changes.swift | 54 ++----- ...loudKitResponseProcessor+ModifyZones.swift | 9 +- .../CloudKitResponseProcessor.swift | 94 ++++-------- .../CloudKitService+AssetOperations.swift | 16 -- .../CloudKitService+AssetUpload.swift | 13 ++ .../CloudKitService+ModifyZones.swift | 19 --- .../CloudKitService+Operations.swift | 15 -- .../CloudKitService+SyncOperations.swift | 10 -- .../CloudKitService+WriteOperations.swift | 36 ++++- .../CloudKitService+ZoneOperations.swift | 21 +-- .../CloudKitService/CloudKitService.swift | 13 ++ .../MistKit/CloudKitService/QuotaHint.swift | 65 ++++++++ .../Models/RecordOperation+EncodedSize.swift | 51 +++++++ .../MistKit/OpenAPI/LoggingMiddleware.swift | 29 +++- ....DiscoverUserIdentities+InvalidEmail.swift | 6 +- ...ServiceTests.FetchChanges+Validation.swift | 65 -------- ...Tests.FetchZoneChanges+ErrorHandling.swift | 12 +- ...dKitServiceTests.LookupZones+Helpers.swift | 25 +++ ...erviceTests.LookupZones+SuccessCases.swift | 19 +++ ...tServiceTests.LookupZones+Validation.swift | 96 ------------ ...tServiceTests.ModifyZones+Validation.swift | 120 --------------- .../CloudKitServiceTests.Query+Helpers.swift | 14 -- ...loudKitServiceTests.Query+Validation.swift | 142 ------------------ ...oudKitServiceTests.SizeLimits+Assets.swift | 111 ++++++++++++++ ...udKitServiceTests.SizeLimits+Records.swift | 109 ++++++++++++++ .../CloudKitServiceTests.SizeLimits.swift} | 14 +- ...KitServiceTests.Upload+ErrorHandling.swift | 12 +- .../CloudKitServiceTests.Upload+Helpers.swift | 51 ++----- ...oudKitServiceTests.Upload+Validation.swift | 58 ------- Tests/MistKitTests/Mocks/ResponseConfig.swift | 19 --- .../MistKitTests/Mocks/ResponseProvider.swift | 5 - .../RecordOperationEncodedSizeTests.swift | 82 ++++++++++ .../LoggingMiddlewareTests+BodyHandling.swift | 106 +++++++++++++ 66 files changed, 874 insertions(+), 877 deletions(-) create mode 100644 Sources/MistKit/CloudKitService/QuotaHint.swift create mode 100644 Sources/MistKit/Models/RecordOperation+EncodedSize.swift delete mode 100644 Tests/MistKitTests/CloudKitService/LookupZones/CloudKitServiceTests.LookupZones+Validation.swift delete mode 100644 Tests/MistKitTests/CloudKitService/ModifyZones/CloudKitServiceTests.ModifyZones+Validation.swift delete mode 100644 Tests/MistKitTests/CloudKitService/Query/CloudKitServiceTests.Query+Validation.swift create mode 100644 Tests/MistKitTests/CloudKitService/SizeLimits/CloudKitServiceTests.SizeLimits+Assets.swift create mode 100644 Tests/MistKitTests/CloudKitService/SizeLimits/CloudKitServiceTests.SizeLimits+Records.swift rename Tests/MistKitTests/{Mocks/ValidationErrorType.swift => CloudKitService/SizeLimits/CloudKitServiceTests.SizeLimits.swift} (83%) create mode 100644 Tests/MistKitTests/Models/RecordOperationEncodedSizeTests.swift create mode 100644 Tests/MistKitTests/OpenAPI/LoggingMiddleware/LoggingMiddlewareTests+BodyHandling.swift diff --git a/Examples/BushelCloud/Sources/BushelCloudKit/DataSources/TheAppleWiki/IPSWParser.swift b/Examples/BushelCloud/Sources/BushelCloudKit/DataSources/TheAppleWiki/IPSWParser.swift index 009dac43..41305593 100644 --- a/Examples/BushelCloud/Sources/BushelCloudKit/DataSources/TheAppleWiki/IPSWParser.swift +++ b/Examples/BushelCloud/Sources/BushelCloudKit/DataSources/TheAppleWiki/IPSWParser.swift @@ -58,7 +58,6 @@ internal enum TheAppleWikiError: LocalizedError { // MARK: - Parser /// Fetches macOS IPSW metadata from TheAppleWiki.com -@available(macOS 12.0, *) internal struct IPSWParser: Sendable { private let baseURL = "https://theapplewiki.com" private let apiEndpoint = "/api.php" diff --git a/Examples/CelestraCloud/Sources/CelestraCloud/Commands/AddFeedCommand.swift b/Examples/CelestraCloud/Sources/CelestraCloud/Commands/AddFeedCommand.swift index 4680eb7a..90438e7d 100644 --- a/Examples/CelestraCloud/Sources/CelestraCloud/Commands/AddFeedCommand.swift +++ b/Examples/CelestraCloud/Sources/CelestraCloud/Commands/AddFeedCommand.swift @@ -35,7 +35,6 @@ import MistKit // MARK: - Main Type internal enum AddFeedCommand { - @available(macOS 11.0, iOS 14.0, tvOS 14.0, watchOS 7.0, *) internal static func run(args: [String]) async throws { guard let feedURL = args.first else { print("Error: Missing feed URL") diff --git a/Examples/CelestraCloud/Sources/CelestraCloud/Commands/ClearCommand.swift b/Examples/CelestraCloud/Sources/CelestraCloud/Commands/ClearCommand.swift index 21e75c1f..b122ec03 100644 --- a/Examples/CelestraCloud/Sources/CelestraCloud/Commands/ClearCommand.swift +++ b/Examples/CelestraCloud/Sources/CelestraCloud/Commands/ClearCommand.swift @@ -33,7 +33,6 @@ import Foundation import MistKit internal enum ClearCommand { - @available(macOS 11.0, iOS 14.0, tvOS 14.0, watchOS 7.0, *) internal static func run(args: [String]) async throws { // Require confirmation let hasConfirm = args.contains("--confirm") diff --git a/Examples/CelestraCloud/Sources/CelestraCloud/Commands/UpdateCommand.swift b/Examples/CelestraCloud/Sources/CelestraCloud/Commands/UpdateCommand.swift index f768ba79..ac3f48ef 100644 --- a/Examples/CelestraCloud/Sources/CelestraCloud/Commands/UpdateCommand.swift +++ b/Examples/CelestraCloud/Sources/CelestraCloud/Commands/UpdateCommand.swift @@ -33,7 +33,6 @@ import Foundation import MistKit internal enum UpdateCommand { - @available(macOS 13.0, *) internal static func run() async throws { let startTime = Date() let loader = ConfigurationLoader() @@ -91,7 +90,6 @@ internal enum UpdateCommand { } } - @available(macOS 13.0, *) private static func createProcessor( config: CelestraConfiguration ) throws -> FeedUpdateProcessor { @@ -115,7 +113,6 @@ internal enum UpdateCommand { ) } - @available(macOS 13.0, *) private static func queryFeeds( config: CelestraConfiguration, processor: FeedUpdateProcessor @@ -138,7 +135,6 @@ internal enum UpdateCommand { return feeds } - @available(macOS 13.0, *) private static func processFeeds( _ feeds: [Feed], processor: FeedUpdateProcessor diff --git a/Examples/CelestraCloud/Sources/CelestraCloud/Services/FeedUpdateProcessor+Fetch.swift b/Examples/CelestraCloud/Sources/CelestraCloud/Services/FeedUpdateProcessor+Fetch.swift index 4cd6e369..236c0605 100644 --- a/Examples/CelestraCloud/Sources/CelestraCloud/Services/FeedUpdateProcessor+Fetch.swift +++ b/Examples/CelestraCloud/Sources/CelestraCloud/Services/FeedUpdateProcessor+Fetch.swift @@ -32,7 +32,6 @@ import CelestraKit import Foundation import MistKit -@available(macOS 13.0, *) extension FeedUpdateProcessor { internal func processSuccessfulFetch( feed: Feed, diff --git a/Examples/CelestraCloud/Sources/CelestraCloud/Services/FeedUpdateProcessor.swift b/Examples/CelestraCloud/Sources/CelestraCloud/Services/FeedUpdateProcessor.swift index d9959e3b..c880c58e 100644 --- a/Examples/CelestraCloud/Sources/CelestraCloud/Services/FeedUpdateProcessor.swift +++ b/Examples/CelestraCloud/Sources/CelestraCloud/Services/FeedUpdateProcessor.swift @@ -33,7 +33,6 @@ import Foundation import MistKit /// Processes individual feed updates -@available(macOS 13.0, *) internal struct FeedUpdateProcessor { internal let service: CloudKitService internal let fetcher: RSSFetcherService diff --git a/Examples/CelestraCloud/Sources/CelestraCloudKit/CelestraConfig.swift b/Examples/CelestraCloud/Sources/CelestraCloudKit/CelestraConfig.swift index 648f902e..7522d099 100644 --- a/Examples/CelestraCloud/Sources/CelestraCloudKit/CelestraConfig.swift +++ b/Examples/CelestraCloud/Sources/CelestraCloudKit/CelestraConfig.swift @@ -35,7 +35,6 @@ public import MistKit /// Shared configuration helper for creating CloudKit service public enum CelestraConfig { /// Create CloudKit service from validated configuration - @available(macOS 11.0, iOS 14.0, tvOS 14.0, watchOS 7.0, *) public static func createCloudKitService(from config: ValidatedCloudKitConfiguration) throws -> CloudKitService { @@ -57,7 +56,6 @@ public enum CelestraConfig { } /// Create CloudKit service from environment variables - @available(macOS 11.0, iOS 14.0, tvOS 14.0, watchOS 7.0, *) @available( *, deprecated, message: "Use ConfigurationLoader with createCloudKitService(from:) instead" ) diff --git a/Examples/CelestraCloud/Sources/CelestraCloudKit/Configuration/ConfigurationLoader.swift b/Examples/CelestraCloud/Sources/CelestraCloudKit/Configuration/ConfigurationLoader.swift index 460de999..b4449c9c 100644 --- a/Examples/CelestraCloud/Sources/CelestraCloudKit/Configuration/ConfigurationLoader.swift +++ b/Examples/CelestraCloud/Sources/CelestraCloudKit/Configuration/ConfigurationLoader.swift @@ -32,7 +32,6 @@ internal import Foundation internal import MistKit /// Loads and merges configuration from multiple sources -@available(macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0, *) public actor ConfigurationLoader { private let configReader: ConfigReader diff --git a/Examples/CelestraCloud/Sources/CelestraCloudKit/Protocols/CloudKitRecordOperating.swift b/Examples/CelestraCloud/Sources/CelestraCloudKit/Protocols/CloudKitRecordOperating.swift index 653c38f1..71f65aee 100644 --- a/Examples/CelestraCloud/Sources/CelestraCloudKit/Protocols/CloudKitRecordOperating.swift +++ b/Examples/CelestraCloud/Sources/CelestraCloudKit/Protocols/CloudKitRecordOperating.swift @@ -76,7 +76,6 @@ public protocol CloudKitRecordOperating: Sendable { // MARK: - CloudKitService Conformance -@available(macOS 11.0, iOS 14.0, tvOS 14.0, watchOS 7.0, *) extension CloudKitService: CloudKitRecordOperating { /// Satisfy CloudKitRecordOperating protocol by forwarding to modifyRecords(_:atomic:) public func modifyRecords(_ operations: [RecordOperation]) async throws(CloudKitError) diff --git a/Examples/CelestraCloud/Sources/CelestraCloudKit/Services/ArticleCategorizer.swift b/Examples/CelestraCloud/Sources/CelestraCloudKit/Services/ArticleCategorizer.swift index c92b5eb8..3d9a8801 100644 --- a/Examples/CelestraCloud/Sources/CelestraCloudKit/Services/ArticleCategorizer.swift +++ b/Examples/CelestraCloud/Sources/CelestraCloudKit/Services/ArticleCategorizer.swift @@ -31,7 +31,6 @@ public import CelestraKit internal import Foundation /// Pure function type for categorizing feed items into new vs modified articles -@available(macOS 13.0, *) public struct ArticleCategorizer: Sendable { /// Result of article categorization public struct Result: Sendable, Equatable { diff --git a/Examples/CelestraCloud/Sources/CelestraCloudKit/Services/ArticleCloudKitService.swift b/Examples/CelestraCloud/Sources/CelestraCloudKit/Services/ArticleCloudKitService.swift index dff70fdd..aeafbe16 100644 --- a/Examples/CelestraCloud/Sources/CelestraCloudKit/Services/ArticleCloudKitService.swift +++ b/Examples/CelestraCloud/Sources/CelestraCloudKit/Services/ArticleCloudKitService.swift @@ -55,7 +55,6 @@ private let guidQueryBatchSize = 150 private let articleMutationBatchSize = 10 /// Service for Article-related CloudKit operations with dependency injection support -@available(macOS 11.0, iOS 14.0, tvOS 14.0, watchOS 7.0, *) public struct ArticleCloudKitService: Sendable { private enum BatchOperation { case create diff --git a/Examples/CelestraCloud/Sources/CelestraCloudKit/Services/ArticleOperationBuilder.swift b/Examples/CelestraCloud/Sources/CelestraCloudKit/Services/ArticleOperationBuilder.swift index 672665ab..fcc13c07 100644 --- a/Examples/CelestraCloud/Sources/CelestraCloudKit/Services/ArticleOperationBuilder.swift +++ b/Examples/CelestraCloud/Sources/CelestraCloudKit/Services/ArticleOperationBuilder.swift @@ -34,7 +34,6 @@ public import MistKit /// Pure function type for building CloudKit record operations from articles. /// Follows the pattern of ArticleCategorizer and FeedMetadataBuilder for testable, /// dependency-free operation building. -@available(macOS 11.0, iOS 14.0, tvOS 14.0, watchOS 7.0, *) public struct ArticleOperationBuilder: Sendable { /// Initialize article operation builder public init() {} diff --git a/Examples/CelestraCloud/Sources/CelestraCloudKit/Services/ArticleSyncService.swift b/Examples/CelestraCloud/Sources/CelestraCloudKit/Services/ArticleSyncService.swift index 3030e513..dd146428 100644 --- a/Examples/CelestraCloud/Sources/CelestraCloudKit/Services/ArticleSyncService.swift +++ b/Examples/CelestraCloud/Sources/CelestraCloudKit/Services/ArticleSyncService.swift @@ -31,7 +31,6 @@ public import CelestraKit public import MistKit /// Service for synchronizing articles: query existing, categorize, create/update -@available(macOS 13.0, *) public struct ArticleSyncService: Sendable { private let articleService: ArticleCloudKitService private let categorizer: ArticleCategorizer diff --git a/Examples/CelestraCloud/Sources/CelestraCloudKit/Services/CelestraError.swift b/Examples/CelestraCloud/Sources/CelestraCloudKit/Services/CelestraError.swift index b71ae8a6..72d34d0c 100644 --- a/Examples/CelestraCloud/Sources/CelestraCloudKit/Services/CelestraError.swift +++ b/Examples/CelestraCloud/Sources/CelestraCloudKit/Services/CelestraError.swift @@ -150,6 +150,13 @@ public enum CelestraError: LocalizedError { case .missingCredentials, .invalidPrivateKey: // Credential/configuration issues — not retriable return false + case .badRequest, .atomicFailure: + // Server-side malformed-request / atomic-batch failures — not retriable + return false + case .quotaExceeded: + // Could be size-limit (not retriable) or storage-quota exhaustion + // (also not retriable until the user frees space). Either way, no. + return false } } } diff --git a/Examples/CelestraCloud/Sources/CelestraCloudKit/Services/CloudKitService+Celestra.swift b/Examples/CelestraCloud/Sources/CelestraCloudKit/Services/CloudKitService+Celestra.swift index 7f5f22a2..8b4cf9a7 100644 --- a/Examples/CelestraCloud/Sources/CelestraCloudKit/Services/CloudKitService+Celestra.swift +++ b/Examples/CelestraCloud/Sources/CelestraCloudKit/Services/CloudKitService+Celestra.swift @@ -33,7 +33,6 @@ internal import Logging public import MistKit /// CloudKit service extensions for Celestra operations -@available(macOS 11.0, iOS 14.0, tvOS 14.0, watchOS 7.0, *) extension CloudKitService { // MARK: - Feed Operations diff --git a/Examples/CelestraCloud/Sources/CelestraCloudKit/Services/FeedCloudKitService.swift b/Examples/CelestraCloud/Sources/CelestraCloudKit/Services/FeedCloudKitService.swift index a4974ef6..48ce59cb 100644 --- a/Examples/CelestraCloud/Sources/CelestraCloudKit/Services/FeedCloudKitService.swift +++ b/Examples/CelestraCloud/Sources/CelestraCloudKit/Services/FeedCloudKitService.swift @@ -33,7 +33,6 @@ import Logging public import MistKit /// Service for Feed-related CloudKit operations with dependency injection support -@available(macOS 11.0, iOS 14.0, tvOS 14.0, watchOS 7.0, *) public struct FeedCloudKitService: Sendable { private let recordOperator: any CloudKitRecordOperating diff --git a/Examples/MistDemo/Sources/MistDemoKit/Commands/DemoErrorsRunner+Output.swift b/Examples/MistDemo/Sources/MistDemoKit/Commands/DemoErrorsRunner+Output.swift index 17fb2b2b..ad28ddc3 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Commands/DemoErrorsRunner+Output.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Commands/DemoErrorsRunner+Output.swift @@ -56,10 +56,23 @@ extension DemoErrorsRunner { let status = error.httpStatusCode.map(String.init) ?? "n/a" let prefix = error.httpStatusCode == expectedStatus ? "✅" : "❌" print("\(prefix) Caught CloudKitError — status: \(status)") - if case .httpErrorWithDetails(_, let serverErrorCode, let reason) = error { + switch error { + case .httpErrorWithDetails(_, let serverErrorCode, let reason): print(" serverErrorCode: \(serverErrorCode ?? "")") print(" reason: \(reason ?? "")") - } else { + case .badRequest(let reason): + print(" serverErrorCode: BAD_REQUEST") + print(" reason: \(reason ?? "")") + case .quotaExceeded(let reason, let hint): + print(" serverErrorCode: QUOTA_EXCEEDED") + print(" reason: \(reason ?? "")") + if let hint { + print(" hint: \(hint.description)") + } + case .atomicFailure(let reason): + print(" serverErrorCode: ATOMIC_ERROR") + print(" reason: \(reason ?? "")") + default: print(" detail: \(error.localizedDescription)") } } diff --git a/Examples/MistDemo/Sources/MistDemoKit/Commands/DemoInFilterCommand.swift b/Examples/MistDemo/Sources/MistDemoKit/Commands/DemoInFilterCommand.swift index 7c3a32e6..b175b536 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Commands/DemoInFilterCommand.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Commands/DemoInFilterCommand.swift @@ -115,7 +115,6 @@ public struct DemoInFilterCommand: MistDemoCommand { return createdNames } - @available(macOS 11.0, iOS 14.0, tvOS 14.0, watchOS 7.0, *) private func verifyAndQueryRecords( client: CloudKitService, recordType: String, diff --git a/Examples/MistDemo/Sources/MistDemoKit/Commands/QueryCommand+FilterParsing.swift b/Examples/MistDemo/Sources/MistDemoKit/Commands/QueryCommand+FilterParsing.swift index fc38ab06..507c1e61 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Commands/QueryCommand+FilterParsing.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Commands/QueryCommand+FilterParsing.swift @@ -32,7 +32,6 @@ import MistKit extension QueryCommand { /// Parse a single filter expression "field:operator:value" into a QueryFilter - @available(macOS 11.0, iOS 14.0, tvOS 14.0, watchOS 7.0, *) internal static func parseFilter(_ filterString: String) throws -> QueryFilter { let components = filterString.split( separator: ":", maxSplits: 2, omittingEmptySubsequences: false @@ -54,7 +53,6 @@ extension QueryCommand { } /// Build a QueryFilter from parsed components. - @available(macOS 11.0, iOS 14.0, tvOS 14.0, watchOS 7.0, *) internal static func buildFilter( field: String, operatorString: String, @@ -71,7 +69,6 @@ extension QueryCommand { } /// Build comparison-based filters (equals, not equals, greater/less than). - @available(macOS 11.0, iOS 14.0, tvOS 14.0, watchOS 7.0, *) // swiftlint:disable:next cyclomatic_complexity internal static func buildComparisonFilter( field: String, @@ -101,7 +98,6 @@ extension QueryCommand { } /// Build string and list-based filters. - @available(macOS 11.0, iOS 14.0, tvOS 14.0, watchOS 7.0, *) internal static func buildSpecialFilter( field: String, operatorString: String, diff --git a/Examples/MistDemo/Sources/MistDemoKit/Commands/QueryCommand.swift b/Examples/MistDemo/Sources/MistDemoKit/Commands/QueryCommand.swift index 4d0a9ea2..d9353970 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Commands/QueryCommand.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Commands/QueryCommand.swift @@ -71,28 +71,17 @@ public struct QueryCommand: MistDemoCommand, OutputFormatting { // Build filters // NOTE: Zone, offset, and continuation marker support require // enhancements to CloudKitService.queryRecords method (GitHub issues #145, #146) - let recordInfos: [RecordInfo] - if #available(macOS 11.0, iOS 14.0, tvOS 14.0, watchOS 7.0, *) { - let filters: [QueryFilter]? = - config.filters.isEmpty - ? nil - : try config.filters.map { try Self.parseFilter($0) } - recordInfos = try await client.queryRecords( - recordType: config.recordType, - filters: filters, - sortBy: nil, - limit: config.limit, - database: config.base.database - ) - } else { - recordInfos = try await client.queryRecords( - recordType: config.recordType, - filters: nil, - sortBy: nil, - limit: config.limit, - database: config.base.database - ) - } + let filters: [QueryFilter]? = + config.filters.isEmpty + ? nil + : try config.filters.map { try Self.parseFilter($0) } + let recordInfos = try await client.queryRecords( + recordType: config.recordType, + filters: filters, + sortBy: nil, + limit: config.limit, + database: config.base.database + ) // Format and output results try await outputResults(recordInfos, format: config.output) diff --git a/Examples/MistDemo/Sources/MistDemoKit/Server/WebBackend.swift b/Examples/MistDemo/Sources/MistDemoKit/Server/WebBackend.swift index ff8039fd..8fff5843 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Server/WebBackend.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Server/WebBackend.swift @@ -68,7 +68,6 @@ internal protocol WebBackend: Sendable { ) async throws } -@available(macOS 11.0, iOS 14.0, tvOS 14.0, watchOS 7.0, *) extension CloudKitService: WebBackend { internal func webQuery( recordType: String, diff --git a/Examples/MistDemo/Tests/MistDemoTests/CloudKit/MistKitClientFactory/MistKitClientFactoryTests+Helpers.swift b/Examples/MistDemo/Tests/MistDemoTests/CloudKit/MistKitClientFactory/MistKitClientFactoryTests+Helpers.swift index 682d78a9..451cfb63 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/CloudKit/MistKitClientFactory/MistKitClientFactoryTests+Helpers.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/CloudKit/MistKitClientFactory/MistKitClientFactoryTests+Helpers.swift @@ -41,14 +41,6 @@ extension MistKitClientFactoryTests { -----END PRIVATE KEY----- """ - internal static func isServerToServerSupported() -> Bool { - if #available(macOS 11.0, iOS 14.0, tvOS 14.0, watchOS 7.0, *) { - return true - } else { - return false - } - } - internal static func makeConfig( containerIdentifier: String = "iCloud.com.test.App", apiToken: String = "test-api-token", diff --git a/Examples/MistDemo/Tests/MistDemoTests/CloudKit/MistKitClientFactory/MistKitClientFactoryTests+ServerToServerAuth.swift b/Examples/MistDemo/Tests/MistDemoTests/CloudKit/MistKitClientFactory/MistKitClientFactoryTests+ServerToServerAuth.swift index ef8c6de1..8b37fb3d 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/CloudKit/MistKitClientFactory/MistKitClientFactoryTests+ServerToServerAuth.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/CloudKit/MistKitClientFactory/MistKitClientFactoryTests+ServerToServerAuth.swift @@ -36,10 +36,7 @@ import Testing extension MistKitClientFactoryTests { @Suite("Server-to-Server Auth") internal struct ServerToServerAuth { - @Test( - "Create client with server-to-server auth", - .enabled(if: MistKitClientFactoryTests.isServerToServerSupported()) - ) + @Test("Create client with server-to-server auth") internal func createWithServerToServerAuth() async throws { let config = try await MistKitClientFactoryTests.makeConfig( apiToken: "api-token", @@ -52,10 +49,7 @@ extension MistKitClientFactoryTests { #expect(client != nil) } - @Test( - "Throw error when server-to-server auth incomplete", - .enabled(if: MistKitClientFactoryTests.isServerToServerSupported()) - ) + @Test("Throw error when server-to-server auth incomplete") internal func throwErrorWhenServerToServerIncomplete() async throws { let config = try await MistKitClientFactoryTests.makeConfig( apiToken: "api-token", diff --git a/Examples/MistDemo/Tests/MistDemoTests/Utilities/AuthenticationHelper/AuthenticationHelperTests+AuthenticationMethodPriority.swift b/Examples/MistDemo/Tests/MistDemoTests/Utilities/AuthenticationHelper/AuthenticationHelperTests+AuthenticationMethodPriority.swift index c2f5bc2d..a2e92d0f 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Utilities/AuthenticationHelper/AuthenticationHelperTests+AuthenticationMethodPriority.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Utilities/AuthenticationHelper/AuthenticationHelperTests+AuthenticationMethodPriority.swift @@ -37,7 +37,6 @@ extension AuthenticationHelperTests { @Suite("Authentication Method Priority") internal struct AuthenticationMethodPriority { @Test("Server-to-server takes precedence over web auth") - @available(macOS 11.0, iOS 14.0, tvOS 14.0, watchOS 7.0, *) internal func serverToServerTakesPrecedence() async throws { let privateKeyPEM = AuthenticationHelperTests.testPrivateKeyPEM diff --git a/Examples/MistDemo/Tests/MistDemoTests/Utilities/AuthenticationHelper/AuthenticationHelperTests+ServerToServerAuthentication.swift b/Examples/MistDemo/Tests/MistDemoTests/Utilities/AuthenticationHelper/AuthenticationHelperTests+ServerToServerAuthentication.swift index 89771f4f..16c50fa4 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Utilities/AuthenticationHelper/AuthenticationHelperTests+ServerToServerAuthentication.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Utilities/AuthenticationHelper/AuthenticationHelperTests+ServerToServerAuthentication.swift @@ -43,7 +43,6 @@ extension AuthenticationHelperTests { "FileManager.temporaryDirectory write isn't supported under WASI sandbox" ) ) - @available(macOS 11.0, iOS 14.0, tvOS 14.0, watchOS 7.0, *) internal func serverToServerAuthWithKeyID() async throws { // Create a temporary private key file let tempDir = FileManager.default.temporaryDirectory @@ -79,7 +78,6 @@ extension AuthenticationHelperTests { } @Test("Server-to-server auth with inline private key") - @available(macOS 11.0, iOS 14.0, tvOS 14.0, watchOS 7.0, *) internal func serverToServerAuthWithInlineKey() async throws { let privateKeyPEM = AuthenticationHelperTests.testPrivateKeyPEM @@ -101,7 +99,6 @@ extension AuthenticationHelperTests { } @Test("Server-to-server auth enforces public database") - @available(macOS 11.0, iOS 14.0, tvOS 14.0, watchOS 7.0, *) internal func serverToServerEnforcesPublicDatabase() async throws { let privateKeyPEM = AuthenticationHelperTests.testPrivateKeyPEM @@ -126,7 +123,6 @@ extension AuthenticationHelperTests { } @Test("Server-to-server auth throws on missing private key") - @available(macOS 11.0, iOS 14.0, tvOS 14.0, watchOS 7.0, *) internal func serverToServerThrowsOnMissingPrivateKey() async throws { do { _ = try await AuthenticationHelper.setupAuthentication( @@ -148,7 +144,6 @@ extension AuthenticationHelperTests { } @Test("Server-to-server auth throws on invalid key file path") - @available(macOS 11.0, iOS 14.0, tvOS 14.0, watchOS 7.0, *) internal func serverToServerThrowsOnInvalidKeyFile() async throws { let invalidPath = "/nonexistent/path/to/key.pem" diff --git a/Package.swift b/Package.swift index 69055848..0cb67ba5 100644 --- a/Package.swift +++ b/Package.swift @@ -84,11 +84,11 @@ let swiftSettings: [SwiftSetting] = [ let package = Package( name: "MistKit", platforms: [ - .macOS(.v10_15), // Minimum for swift-crypto - .iOS(.v13), // Minimum for swift-crypto - .tvOS(.v13), // Minimum for swift-crypto - .watchOS(.v6), // Minimum for swift-crypto - .visionOS(.v1) // Vision OS already requires newer versions + .macOS(.v11), + .iOS(.v14), + .tvOS(.v14), + .watchOS(.v7), + .visionOS(.v1) // Note: WASM/WASI support doesn't require explicit platform declaration // Use --swift-sdk wasm32-unknown-wasi when building for WASM ], diff --git a/README.md b/README.md index afdb88d9..c1f897e8 100644 --- a/README.md +++ b/README.md @@ -77,15 +77,15 @@ Or add it through Xcode: #### Minimum Platform Versions -| Platform | Minimum Version | Server-to-Server Auth | -|----------|-----------------|----------------------| -| macOS | 10.15+ | 11.0+ | -| iOS | 13.0+ | 14.0+ | -| tvOS | 13.0+ | 14.0+ | -| watchOS | 6.0+ | 7.0+ | -| visionOS | 1.0+ | 1.0+ | -| Linux | Ubuntu 18.04+ | ✅ | -| Windows | 10+ | ✅ | +| Platform | Minimum Version | +|----------|-----------------| +| macOS | 11.0+ | +| iOS | 14.0+ | +| tvOS | 14.0+ | +| watchOS | 7.0+ | +| visionOS | 1.0+ | +| Linux | Ubuntu 18.04+ | +| Windows | 10+ | ### Quick Start diff --git a/Sources/MistKit/Authentication/Credentials+TokenManager.swift b/Sources/MistKit/Authentication/Credentials+TokenManager.swift index d7267d8b..f16ab7dc 100644 --- a/Sources/MistKit/Authentication/Credentials+TokenManager.swift +++ b/Sources/MistKit/Authentication/Credentials+TokenManager.swift @@ -75,9 +75,7 @@ extension Credentials { auth: PublicAuthPreference ) throws -> any TokenManager { if let s2s = serverToServer { - if #available(macOS 11.0, iOS 14.0, tvOS 14.0, watchOS 7.0, *) { - return try makeServerToServerManager(s2s) - } + return try makeServerToServerManager(s2s) } if auth.required { throw CloudKitError.missingCredentials( @@ -115,9 +113,7 @@ extension Credentials { ) } if let s2s = serverToServer { - if #available(macOS 11.0, iOS 14.0, tvOS 14.0, watchOS 7.0, *) { - return try makeServerToServerManager(s2s) - } + return try makeServerToServerManager(s2s) } if let api = apiAuth { return makeAPITokenManager(api) @@ -146,7 +142,6 @@ extension Credentials { ) } - @available(macOS 11.0, iOS 14.0, tvOS 14.0, watchOS 7.0, *) private func makeServerToServerManager( _ s2s: ServerToServerCredentials ) throws -> any TokenManager { diff --git a/Sources/MistKit/Authentication/HashFunction+CloudKitBodyHash.swift b/Sources/MistKit/Authentication/HashFunction+CloudKitBodyHash.swift index 3fce1df5..29c260d1 100644 --- a/Sources/MistKit/Authentication/HashFunction+CloudKitBodyHash.swift +++ b/Sources/MistKit/Authentication/HashFunction+CloudKitBodyHash.swift @@ -30,7 +30,6 @@ internal import Crypto internal import Foundation -@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) extension HashFunction { /// Returns the base64-encoded hash of the given body, or the empty /// string when `body` is nil — matching CloudKit Web Services' convention diff --git a/Sources/MistKit/Authentication/ServerToServerAuthManager.swift b/Sources/MistKit/Authentication/ServerToServerAuthManager.swift index 9183c904..44fb944f 100644 --- a/Sources/MistKit/Authentication/ServerToServerAuthManager.swift +++ b/Sources/MistKit/Authentication/ServerToServerAuthManager.swift @@ -32,7 +32,6 @@ public import Foundation /// Token manager for server-to-server authentication using ECDSA P-256 signing. /// Provides enterprise-level authentication for CloudKit Web Services. -/// Available on macOS 11.0+, iOS 14.0+, tvOS 14.0+, watchOS 7.0+, and Linux. public final class ServerToServerAuthManager: TokenManager, Sendable { internal let keyID: String internal let privateKey: P256.Signing.PrivateKey @@ -83,7 +82,6 @@ public final class ServerToServerAuthManager: TokenManager, Sendable { } /// Convenience initializer with PEM-formatted private key. - @available(macOS 11.0, iOS 14.0, tvOS 14.0, watchOS 7.0, *) public convenience init( keyID: String, pemString: String, diff --git a/Sources/MistKit/Authentication/ServerToServerAuthenticator.swift b/Sources/MistKit/Authentication/ServerToServerAuthenticator.swift index dd285d50..a263fc5d 100644 --- a/Sources/MistKit/Authentication/ServerToServerAuthenticator.swift +++ b/Sources/MistKit/Authentication/ServerToServerAuthenticator.swift @@ -118,7 +118,6 @@ public struct ServerToServerAuthenticator: Authenticator { } /// Convenience initializer with a PEM-encoded private key string. - @available(macOS 11.0, iOS 14.0, tvOS 14.0, watchOS 7.0, *) public init( keyID: String, pemString: String, diff --git a/Sources/MistKit/CloudKitService/CloudKitError+OpenAPI.swift b/Sources/MistKit/CloudKitService/CloudKitError+OpenAPI.swift index 8b91f8cd..b663404a 100644 --- a/Sources/MistKit/CloudKitService/CloudKitError+OpenAPI.swift +++ b/Sources/MistKit/CloudKitService/CloudKitError+OpenAPI.swift @@ -43,14 +43,59 @@ extension CloudKitError { /// Build a `CloudKitError` from any CloudKit failure response. /// The body schema is identical across status codes — only the code /// disambiguates which CloudKit failure occurred, so the caller supplies it. + /// + /// Three server codes are surfaced as dedicated cases: + /// - `QUOTA_EXCEEDED` → `.quotaExceeded(reason:, hint: nil)` — the catch + /// block in the calling operation may enrich `hint` from local context. + /// - `BAD_REQUEST` → `.badRequest(reason:)` + /// - `ATOMIC_ERROR` → `.atomicFailure(reason:)` + /// + /// Every other server code lands in `.httpErrorWithDetails`. internal init(_ response: Components.Responses.Failure, statusCode: Int) { switch response.body { case .json(let errorResponse): - self = .httpErrorWithDetails( - statusCode: statusCode, - serverErrorCode: errorResponse.serverErrorCode?.rawValue, - reason: errorResponse.reason - ) + let code = errorResponse.serverErrorCode?.rawValue + let reason = errorResponse.reason + switch code { + case "QUOTA_EXCEEDED": + self = .quotaExceeded(reason: reason, hint: nil) + case "BAD_REQUEST": + self = .badRequest(reason: reason) + case "ATOMIC_ERROR": + self = .atomicFailure(reason: reason) + default: + self = .httpErrorWithDetails( + statusCode: statusCode, + serverErrorCode: code, + reason: reason + ) + } + } + } + + /// Returns a copy of this error with the given hint attached. + /// + /// If `self` is already `.quotaExceeded`, the existing reason is preserved + /// and the hint is replaced. If `self` is a bare 413 (`.httpError(413)` or + /// `.httpErrorWithDetails(413, …)`) — e.g., from the CDN asset-upload + /// endpoint, which returns raw HTTP errors rather than CloudKit JSON — + /// the error is upgraded to `.quotaExceeded` with the hint attached. + /// Other cases are returned unchanged. `nil` hint is always a no-op. + /// + /// Used by operation catch blocks to enrich a server-returned quota error + /// with information that can only be computed from the local request state + /// (e.g., the actual encoded record size, the asset byte count). + internal func addingQuotaHint(_ hint: QuotaHint?) -> CloudKitError { + guard let hint else { return self } + switch self { + case .quotaExceeded(let reason, _): + return .quotaExceeded(reason: reason, hint: hint) + case .httpError(let statusCode) where statusCode == 413: + return .quotaExceeded(reason: nil, hint: hint) + case .httpErrorWithDetails(let statusCode, _, let reason) where statusCode == 413: + return .quotaExceeded(reason: reason, hint: hint) + default: + return self } } diff --git a/Sources/MistKit/CloudKitService/CloudKitError.swift b/Sources/MistKit/CloudKitService/CloudKitError.swift index 4d5d416e..3d511f6a 100644 --- a/Sources/MistKit/CloudKitService/CloudKitError.swift +++ b/Sources/MistKit/CloudKitService/CloudKitError.swift @@ -37,8 +37,21 @@ import OpenAPIRuntime /// Represents errors that can occur when interacting with CloudKit Web Services public enum CloudKitError: LocalizedError, Sendable { case httpError(statusCode: Int) + /// Server-returned error for `serverErrorCode` values **other than** + /// `QUOTA_EXCEEDED`, `BAD_REQUEST`, and `ATOMIC_ERROR` — those have their own + /// dedicated cases (`.quotaExceeded`, `.badRequest`, `.atomicFailure`). case httpErrorWithDetails(statusCode: Int, serverErrorCode: String?, reason: String?) case httpErrorWithRawResponse(statusCode: Int, rawResponse: String) + /// HTTP 413 / `QUOTA_EXCEEDED`. Same server code is used for storage-quota + /// exhaustion and per-record / per-asset size limits; `hint` (when non-nil) + /// disambiguates from local request context. + case quotaExceeded(reason: String?, hint: QuotaHint?) + /// HTTP 400 / `BAD_REQUEST`. The server's `reason` describes the specific + /// malformed input. + case badRequest(reason: String?) + /// HTTP 400 / `ATOMIC_ERROR`. A `modifyRecords` call with `atomic: true` + /// rolled back because at least one operation in the batch failed. + case atomicFailure(reason: String?) case invalidResponse case underlyingError(any Error) case decodingError(DecodingError) @@ -59,6 +72,10 @@ public enum CloudKitError: LocalizedError, Sendable { .httpErrorWithDetails(let statusCode, _, _), .httpErrorWithRawResponse(let statusCode, _): return statusCode + case .quotaExceeded: + return 413 + case .badRequest, .atomicFailure: + return 400 case .invalidResponse, .underlyingError, .decodingError, .networkError, .unsupportedOperationType, .paginationLimitExceeded, .missingCredentials, .invalidPrivateKey: @@ -146,6 +163,27 @@ public enum CloudKitError: LocalizedError, Sendable { let location = path.map { "from '\($0)'" } ?? "from inline material" return "Failed to load CloudKit private key \(location): \(underlying.localizedDescription)" + case .quotaExceeded(let reason, let hint): + var message = "CloudKit quota exceeded (HTTP 413 / QUOTA_EXCEEDED)" + if let reason { + message += "\nReason: \(reason)" + } + if let hint { + message += "\nHint: \(hint.description)" + } + return message + case .badRequest(let reason): + var message = "CloudKit bad request (HTTP 400 / BAD_REQUEST)" + if let reason { + message += "\nReason: \(reason)" + } + return message + case .atomicFailure(let reason): + var message = "CloudKit atomic batch failure (HTTP 400 / ATOMIC_ERROR)" + if let reason { + message += "\nReason: \(reason)" + } + return message } } } diff --git a/Sources/MistKit/CloudKitService/CloudKitResponseProcessor+Changes.swift b/Sources/MistKit/CloudKitService/CloudKitResponseProcessor+Changes.swift index 84fe8e68..7d5faf8e 100644 --- a/Sources/MistKit/CloudKitService/CloudKitResponseProcessor+Changes.swift +++ b/Sources/MistKit/CloudKitService/CloudKitResponseProcessor+Changes.swift @@ -39,19 +39,16 @@ extension CloudKitResponseProcessor { internal func processFetchRecordChangesResponse(_ response: Operations.fetchRecordChanges.Output) async throws(CloudKitError) -> Components.Schemas.ChangesResponse { - if let error = CloudKitError(response) { - throw error - } switch response { case .ok(let okResponse): switch okResponse.body { case .json(let changesData): return changesData } - default: - // Should never reach here since all errors are handled above - assertionFailure("Unexpected response case after error handling") - throw CloudKitError.invalidResponse + case .badRequest, .unauthorized, .forbidden, .notFound, .conflict, + .preconditionFailed, .contentTooLarge, .misdirectedRequest, + .tooManyRequests, .internalServerError, .serviceUnavailable, .undocumented: + throw CloudKitError(response) ?? .invalidResponse } } @@ -59,19 +56,14 @@ extension CloudKitResponseProcessor { internal func processDiscoverUserIdentitiesResponse( _ response: Operations.discoverUserIdentities.Output ) async throws(CloudKitError) -> Components.Schemas.DiscoverResponse { - if let error = CloudKitError(response) { - throw error - } switch response { case .ok(let okResponse): switch okResponse.body { case .json(let discoverData): return discoverData } - default: - // Should never reach here since all errors are handled above - assertionFailure("Unexpected response case after error handling") - throw CloudKitError.invalidResponse + case .badRequest, .unauthorized, .undocumented: + throw CloudKitError(response) ?? .invalidResponse } } @@ -79,18 +71,14 @@ extension CloudKitResponseProcessor { internal func processLookupUsersByEmailResponse( _ response: Operations.lookupUsersByEmail.Output ) async throws(CloudKitError) -> Components.Schemas.DiscoverResponse { - if let error = CloudKitError(response) { - throw error - } switch response { case .ok(let okResponse): switch okResponse.body { case .json(let discoverData): return discoverData } - default: - assertionFailure("Unexpected response case after error handling") - throw CloudKitError.invalidResponse + case .badRequest, .unauthorized, .undocumented: + throw CloudKitError(response) ?? .invalidResponse } } @@ -98,18 +86,14 @@ extension CloudKitResponseProcessor { internal func processLookupUsersByRecordNameResponse( _ response: Operations.lookupUsersByRecordName.Output ) async throws(CloudKitError) -> Components.Schemas.DiscoverResponse { - if let error = CloudKitError(response) { - throw error - } switch response { case .ok(let okResponse): switch okResponse.body { case .json(let discoverData): return discoverData } - default: - assertionFailure("Unexpected response case after error handling") - throw CloudKitError.invalidResponse + case .badRequest, .unauthorized, .undocumented: + throw CloudKitError(response) ?? .invalidResponse } } @@ -120,19 +104,14 @@ extension CloudKitResponseProcessor { internal func processUploadAssetsResponse(_ response: Operations.uploadAssets.Output) async throws(CloudKitError) -> Components.Schemas.AssetUploadResponse { - if let error = CloudKitError(response) { - throw error - } switch response { case .ok(let okResponse): switch okResponse.body { case .json(let uploadData): return uploadData } - default: - // Should never reach here since all errors are handled above - assertionFailure("Unexpected response case after error handling") - throw CloudKitError.invalidResponse + case .badRequest, .unauthorized, .undocumented: + throw CloudKitError(response) ?? .invalidResponse } } @@ -140,19 +119,14 @@ extension CloudKitResponseProcessor { internal func processFetchZoneChangesResponse(_ response: Operations.fetchZoneChanges.Output) async throws(CloudKitError) -> Components.Schemas.ZoneChangesResponse { - if let error = CloudKitError(response) { - throw error - } switch response { case .ok(let okResponse): switch okResponse.body { case .json(let changesData): return changesData } - default: - // Should never reach here since all errors are handled above - assertionFailure("Unexpected response case after error handling") - throw CloudKitError.invalidResponse + case .badRequest, .unauthorized, .undocumented: + throw CloudKitError(response) ?? .invalidResponse } } } diff --git a/Sources/MistKit/CloudKitService/CloudKitResponseProcessor+ModifyZones.swift b/Sources/MistKit/CloudKitService/CloudKitResponseProcessor+ModifyZones.swift index 93467348..4c204b33 100644 --- a/Sources/MistKit/CloudKitService/CloudKitResponseProcessor+ModifyZones.swift +++ b/Sources/MistKit/CloudKitService/CloudKitResponseProcessor+ModifyZones.swift @@ -37,19 +37,14 @@ extension CloudKitResponseProcessor { internal func processModifyZonesResponse(_ response: Operations.modifyZones.Output) async throws(CloudKitError) -> Components.Schemas.ZonesModifyResponse { - if let error = CloudKitError(response) { - throw error - } - switch response { case .ok(let okResponse): switch okResponse.body { case .json(let zonesData): return zonesData } - default: - assertionFailure("Unexpected response case after error handling") - throw CloudKitError.invalidResponse + case .badRequest, .unauthorized, .undocumented: + throw CloudKitError(response) ?? .invalidResponse } } } diff --git a/Sources/MistKit/CloudKitService/CloudKitResponseProcessor.swift b/Sources/MistKit/CloudKitService/CloudKitResponseProcessor.swift index df5472bb..dd4487b4 100644 --- a/Sources/MistKit/CloudKitService/CloudKitResponseProcessor.swift +++ b/Sources/MistKit/CloudKitService/CloudKitResponseProcessor.swift @@ -32,7 +32,11 @@ internal import Logging internal import MistKitOpenAPI import OpenAPIRuntime -/// Processes CloudKit API responses and handles errors +/// Processes CloudKit API responses and handles errors. +/// +/// Each `processXxxResponse` switches over the response exhaustively — no +/// `default:` clause — so the compiler flags this file when the OpenAPI +/// generator emits a new response variant. internal struct CloudKitResponseProcessor { /// Process getCaller response /// - Parameter response: The response to process @@ -41,19 +45,13 @@ internal struct CloudKitResponseProcessor { internal func processGetCallerResponse(_ response: Operations.getCaller.Output) async throws(CloudKitError) -> Components.Schemas.UserResponse { - // Check for errors first - if let error = CloudKitError(response) { - throw error - } - - // Must be .ok case - extract data switch response { case .ok(let okResponse): return try extractUserData(from: okResponse) - default: - // Should never reach here since all errors are handled above - assertionFailure("Unexpected response case after error handling") - throw CloudKitError.invalidResponse + case .badRequest, .unauthorized, .forbidden, .notFound, .conflict, + .preconditionFailed, .contentTooLarge, .misdirectedRequest, + .tooManyRequests, .internalServerError, .serviceUnavailable, .undocumented: + throw CloudKitError(response) ?? .invalidResponse } } @@ -74,22 +72,16 @@ internal struct CloudKitResponseProcessor { internal func processLookupRecordsResponse(_ response: Operations.lookupRecords.Output) async throws(CloudKitError) -> Components.Schemas.LookupResponse { - // Check for errors first - if let error = CloudKitError(response) { - throw error - } - - // Must be .ok case - extract data switch response { case .ok(let okResponse): switch okResponse.body { case .json(let lookupData): return lookupData } - default: - // Should never reach here since all errors are handled above - assertionFailure("Unexpected response case after error handling") - throw CloudKitError.invalidResponse + case .badRequest, .unauthorized, .forbidden, .notFound, .conflict, + .preconditionFailed, .contentTooLarge, .misdirectedRequest, + .tooManyRequests, .internalServerError, .serviceUnavailable, .undocumented: + throw CloudKitError(response) ?? .invalidResponse } } @@ -101,22 +93,16 @@ internal struct CloudKitResponseProcessor { async throws(CloudKitError) -> Components.Schemas.ZonesListResponse { - // Check for errors first - if let error = CloudKitError(response) { - throw error - } - - // Must be .ok case - extract data switch response { case .ok(let okResponse): switch okResponse.body { case .json(let zonesData): return zonesData } - default: - // Should never reach here since all errors are handled above - assertionFailure("Unexpected response case after error handling") - throw CloudKitError.invalidResponse + case .badRequest, .unauthorized, .forbidden, .notFound, .conflict, + .preconditionFailed, .contentTooLarge, .misdirectedRequest, + .tooManyRequests, .internalServerError, .serviceUnavailable, .undocumented: + throw CloudKitError(response) ?? .invalidResponse } } @@ -127,25 +113,19 @@ internal struct CloudKitResponseProcessor { internal func processQueryRecordsResponse(_ response: Operations.queryRecords.Output) async throws(CloudKitError) -> Components.Schemas.QueryResponse { - // Check for errors first - if let error = CloudKitError(response) { - Logger(subsystem: .api).error( - "CloudKit queryRecords failed with response: \(response)" - ) - throw error - } - - // Must be .ok case - extract data switch response { case .ok(let okResponse): switch okResponse.body { case .json(let recordsData): return recordsData } - default: - // Should never reach here since all errors are handled above - assertionFailure("Unexpected response case after error handling") - throw CloudKitError.invalidResponse + case .badRequest, .unauthorized, .forbidden, .notFound, .conflict, + .preconditionFailed, .contentTooLarge, .misdirectedRequest, + .tooManyRequests, .internalServerError, .serviceUnavailable, .undocumented: + Logger(subsystem: .api).error( + "CloudKit queryRecords failed with response: \(response)" + ) + throw CloudKitError(response) ?? .invalidResponse } } @@ -156,22 +136,16 @@ internal struct CloudKitResponseProcessor { internal func processModifyRecordsResponse(_ response: Operations.modifyRecords.Output) async throws(CloudKitError) -> Components.Schemas.ModifyResponse { - // Check for errors first - if let error = CloudKitError(response) { - throw error - } - - // Must be .ok case - extract data switch response { case .ok(let okResponse): switch okResponse.body { case .json(let modifyData): return modifyData } - default: - // Should never reach here since all errors are handled above - assertionFailure("Unexpected response case after error handling") - throw CloudKitError.invalidResponse + case .badRequest, .unauthorized, .forbidden, .notFound, .conflict, + .preconditionFailed, .contentTooLarge, .misdirectedRequest, + .tooManyRequests, .internalServerError, .serviceUnavailable, .undocumented: + throw CloudKitError(response) ?? .invalidResponse } } @@ -182,22 +156,14 @@ internal struct CloudKitResponseProcessor { internal func processLookupZonesResponse(_ response: Operations.lookupZones.Output) async throws(CloudKitError) -> Components.Schemas.ZonesLookupResponse { - // Check for errors first - if let error = CloudKitError(response) { - throw error - } - - // Must be .ok case - extract data switch response { case .ok(let okResponse): switch okResponse.body { case .json(let zonesData): return zonesData } - default: - // Should never reach here since all errors are handled above - assertionFailure("Unexpected response case after error handling") - throw CloudKitError.invalidResponse + case .badRequest, .unauthorized, .undocumented: + throw CloudKitError(response) ?? .invalidResponse } } } diff --git a/Sources/MistKit/CloudKitService/CloudKitService+AssetOperations.swift b/Sources/MistKit/CloudKitService/CloudKitService+AssetOperations.swift index fbb9ce05..953a492a 100644 --- a/Sources/MistKit/CloudKitService/CloudKitService+AssetOperations.swift +++ b/Sources/MistKit/CloudKitService/CloudKitService+AssetOperations.swift @@ -79,22 +79,6 @@ extension CloudKitService { using uploader: AssetUploader? = nil, database: Database ) async throws(CloudKitError) -> AssetUploadReceipt { - let maxSize: Int = 15 * 1_024 * 1_024 - guard data.count <= maxSize else { - throw CloudKitError.httpErrorWithRawResponse( - statusCode: 413, - rawResponse: - "Asset size \(data.count) exceeds maximum of \(maxSize) bytes" - ) - } - - guard !data.isEmpty else { - throw CloudKitError.httpErrorWithRawResponse( - statusCode: 400, - rawResponse: "Asset data cannot be empty" - ) - } - do { let urlToken = try await requestAssetUploadURL( recordType: recordType, diff --git a/Sources/MistKit/CloudKitService/CloudKitService+AssetUpload.swift b/Sources/MistKit/CloudKitService/CloudKitService+AssetUpload.swift index ff46f684..aba78495 100644 --- a/Sources/MistKit/CloudKitService/CloudKitService+AssetUpload.swift +++ b/Sources/MistKit/CloudKitService/CloudKitService+AssetUpload.swift @@ -98,6 +98,19 @@ extension CloudKitService { ) } catch { throw mapToCloudKitError(error, context: "uploadAssetData") + .addingQuotaHint(Self.assetSizeQuotaHint(for: data)) } } + + /// Returns a `.assetExceedsSizeLimit` hint when local `data` is over + /// CloudKit's per-asset upload limit. Returns `nil` otherwise (the + /// `QUOTA_EXCEEDED` is presumably caused by the user's iCloud storage + /// being full, not the asset size). + private static func assetSizeQuotaHint(for data: Data) -> QuotaHint? { + guard data.count > maxAssetUploadBytes else { return nil } + return .assetExceedsSizeLimit( + dataBytes: data.count, + maxBytes: maxAssetUploadBytes + ) + } } diff --git a/Sources/MistKit/CloudKitService/CloudKitService+ModifyZones.swift b/Sources/MistKit/CloudKitService/CloudKitService+ModifyZones.swift index f931dc94..bad9fd7a 100644 --- a/Sources/MistKit/CloudKitService/CloudKitService+ModifyZones.swift +++ b/Sources/MistKit/CloudKitService/CloudKitService+ModifyZones.swift @@ -67,25 +67,6 @@ extension CloudKitService { _ operations: [ZoneOperation], database: Database ) async throws(CloudKitError) -> [ZoneInfo] { - guard !operations.isEmpty else { - throw CloudKitError.httpErrorWithRawResponse( - statusCode: 400, - rawResponse: "operations cannot be empty" - ) - } - guard operations.allSatisfy({ !$0.zoneID.zoneName.isEmpty }) else { - throw CloudKitError.httpErrorWithRawResponse( - statusCode: 400, - rawResponse: "operations contains a zone with an empty zoneName" - ) - } - if case .public = database { - throw CloudKitError.httpErrorWithRawResponse( - statusCode: 400, - rawResponse: "modifyZones is not supported on the public database" - ) - } - do { let client = try self.client(for: database) let response = try await client.modifyZones( diff --git a/Sources/MistKit/CloudKitService/CloudKitService+Operations.swift b/Sources/MistKit/CloudKitService/CloudKitService+Operations.swift index cdc580a4..2e0d6e25 100644 --- a/Sources/MistKit/CloudKitService/CloudKitService+Operations.swift +++ b/Sources/MistKit/CloudKitService/CloudKitService+Operations.swift @@ -155,21 +155,6 @@ extension CloudKitService { ) async throws(CloudKitError) -> QueryResult { let effectiveLimit = limit ?? defaultQueryLimit - guard !recordType.isEmpty else { - throw CloudKitError.httpErrorWithRawResponse( - statusCode: 400, - rawResponse: "recordType cannot be empty" - ) - } - - guard effectiveLimit > 0 && effectiveLimit <= 200 else { - throw CloudKitError.httpErrorWithRawResponse( - statusCode: 400, - rawResponse: - "limit must be between 1 and 200, got \(effectiveLimit)" - ) - } - let componentsFilters = filters?.map { Components.Schemas.Filter(from: $0) } diff --git a/Sources/MistKit/CloudKitService/CloudKitService+SyncOperations.swift b/Sources/MistKit/CloudKitService/CloudKitService+SyncOperations.swift index ba3fc145..81ffa95d 100644 --- a/Sources/MistKit/CloudKitService/CloudKitService+SyncOperations.swift +++ b/Sources/MistKit/CloudKitService/CloudKitService+SyncOperations.swift @@ -84,16 +84,6 @@ extension CloudKitService { resultsLimit: Int? = nil, database: Database ) async throws(CloudKitError) -> RecordChangesResult { - if let limit = resultsLimit { - guard limit > 0 && limit <= 200 else { - throw CloudKitError.httpErrorWithRawResponse( - statusCode: 400, - rawResponse: - "resultsLimit must be between 1 and 200, got \(limit)" - ) - } - } - let effectiveZoneID = zoneID ?? .defaultZone do { diff --git a/Sources/MistKit/CloudKitService/CloudKitService+WriteOperations.swift b/Sources/MistKit/CloudKitService/CloudKitService+WriteOperations.swift index a34fff9f..5708b937 100644 --- a/Sources/MistKit/CloudKitService/CloudKitService+WriteOperations.swift +++ b/Sources/MistKit/CloudKitService/CloudKitService+WriteOperations.swift @@ -52,11 +52,18 @@ extension CloudKitService { atomic: Bool = false, database: Database ) async throws(CloudKitError) -> [RecordInfo] { + let apiOperations: [Components.Schemas.RecordOperation] do { - let apiOperations = try operations.map { + apiOperations = try operations.map { try Components.Schemas.RecordOperation(from: $0) } + } catch let cloudKitError as CloudKitError { + throw cloudKitError + } catch { + throw CloudKitError.underlyingError(error) + } + do { let client = try self.client(for: database) let response = try await client.modifyRecords( .init( @@ -81,12 +88,37 @@ extension CloudKitService { return modifyResponse.records?.compactMap { RecordInfo(from: $0) } ?? [] } catch let cloudKitError as CloudKitError { - throw cloudKitError + throw cloudKitError.addingQuotaHint( + Self.recordSizeQuotaHint(for: apiOperations) + ) } catch { throw CloudKitError.underlyingError(error) } } + /// Inspect a batch of API record operations and return a `QuotaHint` for + /// the first record whose JSON-encoded size exceeds CloudKit's per-record + /// limit. Returns `nil` if every record is within bounds — which is the + /// usual case when the server's `QUOTA_EXCEEDED` is caused by storage-quota + /// exhaustion rather than per-record size. + private static func recordSizeQuotaHint( + for apiOperations: [Components.Schemas.RecordOperation] + ) -> QuotaHint? { + let encoder = JSONEncoder() + for (index, operation) in apiOperations.enumerated() { + guard let record = operation.record, + let encoded = try? encoder.encode(record), + encoded.count > maxRecordDataBytes + else { continue } + return .recordExceedsSizeLimit( + operationIndex: index, + encodedBytes: encoded.count, + maxBytes: maxRecordDataBytes + ) + } + return nil + } + /// Create a single record in CloudKit /// - Parameters: /// - recordType: The type of record to create diff --git a/Sources/MistKit/CloudKitService/CloudKitService+ZoneOperations.swift b/Sources/MistKit/CloudKitService/CloudKitService+ZoneOperations.swift index 9e32b85a..a180e56f 100644 --- a/Sources/MistKit/CloudKitService/CloudKitService+ZoneOperations.swift +++ b/Sources/MistKit/CloudKitService/CloudKitService+ZoneOperations.swift @@ -63,11 +63,11 @@ extension CloudKitService { let zonesData: Components.Schemas.ZonesListResponse = try await responseProcessor.processListZonesResponse(response) return zonesData.zones?.compactMap { zone in - guard let zoneID = zone.zoneID else { + guard let zoneID = zone.zoneID, let zoneName = zoneID.zoneName else { return nil } return ZoneInfo( - zoneName: zoneID.zoneName ?? "Unknown", + zoneName: zoneName, ownerRecordName: zoneID.ownerName, capabilities: [] ) @@ -102,19 +102,6 @@ extension CloudKitService { zoneIDs: [ZoneID], database: Database = .private ) async throws(CloudKitError) -> [ZoneInfo] { - guard !zoneIDs.isEmpty else { - throw CloudKitError.httpErrorWithRawResponse( - statusCode: 400, - rawResponse: "zoneIDs cannot be empty" - ) - } - guard zoneIDs.allSatisfy({ !$0.zoneName.isEmpty }) else { - throw CloudKitError.httpErrorWithRawResponse( - statusCode: 400, - rawResponse: "zoneIDs contains a zone with an empty zoneName" - ) - } - do { let client = try self.client(for: database) let response = try await client.lookupZones( @@ -136,11 +123,11 @@ extension CloudKitService { try await responseProcessor.processLookupZonesResponse(response) return zonesData.zones?.compactMap { zone in - guard let zoneID = zone.zoneID else { + guard let zoneID = zone.zoneID, let zoneName = zoneID.zoneName else { return nil } return ZoneInfo( - zoneName: zoneID.zoneName ?? "Unknown", + zoneName: zoneName, ownerRecordName: zoneID.ownerName, capabilities: [] ) diff --git a/Sources/MistKit/CloudKitService/CloudKitService.swift b/Sources/MistKit/CloudKitService/CloudKitService.swift index ca3dffac..02b06e01 100644 --- a/Sources/MistKit/CloudKitService/CloudKitService.swift +++ b/Sources/MistKit/CloudKitService/CloudKitService.swift @@ -61,6 +61,19 @@ public struct CloudKitService: Sendable { /// CloudKit's maximum number of records returned per query/modify request. internal static let maxRecordsPerRequest: Int = 200 + /// CloudKit's documented per-record field-data limit (1 MB). Assets travel + /// via the CDN and don't count against this limit. MistKit does not + /// pre-flight this — `modifyRecords` lets CloudKit reject oversized records + /// — but callers who want to check ahead can compare + /// `RecordOperation.encodedRecordSize()` against this constant. + public static let maxRecordDataBytes: Int = 1_024 * 1_024 + + /// CloudKit's documented per-asset upload limit (15 MB). MistKit does not + /// pre-flight this — `uploadAssets`/`uploadAssetData` let the CDN reject — + /// but callers who want to check ahead can compare `data.count` against + /// this constant. + public static let maxAssetUploadBytes: Int = 15 * 1_024 * 1_024 + /// The CloudKit container identifier public let containerIdentifier: String /// The CloudKit environment (development or production) diff --git a/Sources/MistKit/CloudKitService/QuotaHint.swift b/Sources/MistKit/CloudKitService/QuotaHint.swift new file mode 100644 index 00000000..d106e432 --- /dev/null +++ b/Sources/MistKit/CloudKitService/QuotaHint.swift @@ -0,0 +1,65 @@ +// +// QuotaHint.swift +// MistKit +// +// Created by Leo Dion. +// Copyright © 2026 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +/// Local-context diagnostic attached to `CloudKitError.quotaExceeded`. +/// +/// CloudKit returns `QUOTA_EXCEEDED` (HTTP 413) for several distinct +/// conditions — per-record field-data limit, per-asset upload limit, and +/// per-app storage quota exhaustion — all under the same `serverErrorCode`. +/// When MistKit can identify a probable cause from the request that triggered +/// the error, it attaches the matching `QuotaHint` so the caller doesn't have +/// to reconstruct what happened from the server's free-form `reason` string. +/// +/// A `nil` hint means MistKit either had no relevant local context or the +/// request didn't trip any known limit — most likely the user's iCloud quota +/// is exhausted. +public enum QuotaHint: Sendable, Equatable { + /// A record in a `modifyRecords` batch had a JSON-encoded size larger than + /// CloudKit's per-record limit (`CloudKitService.maxRecordDataBytes`). + /// The asset blob is not counted here — it travels via the CDN. + case recordExceedsSizeLimit(operationIndex: Int, encodedBytes: Int, maxBytes: Int) + + /// Asset data passed to `uploadAssets` / `uploadAssetData` was larger than + /// CloudKit's per-asset upload limit (`CloudKitService.maxAssetUploadBytes`). + case assetExceedsSizeLimit(dataBytes: Int, maxBytes: Int) + + /// Human-readable summary used in `CloudKitError.errorDescription`. + public var description: String { + switch self { + case .recordExceedsSizeLimit(let index, let encoded, let max): + return + "operations[\(index)] encoded to \(encoded) bytes " + + "(limit \(max) bytes / \(max / 1_024 / 1_024) MB)" + case .assetExceedsSizeLimit(let data, let max): + return + "asset data was \(data) bytes " + + "(limit \(max) bytes / \(max / 1_024 / 1_024) MB)" + } + } +} diff --git a/Sources/MistKit/Models/RecordOperation+EncodedSize.swift b/Sources/MistKit/Models/RecordOperation+EncodedSize.swift new file mode 100644 index 00000000..ddd0b80d --- /dev/null +++ b/Sources/MistKit/Models/RecordOperation+EncodedSize.swift @@ -0,0 +1,51 @@ +// +// RecordOperation+EncodedSize.swift +// MistKit +// +// Created by Leo Dion. +// Copyright © 2026 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +internal import Foundation +internal import MistKitOpenAPI + +extension RecordOperation { + /// Size in bytes of this operation's record envelope when JSON-encoded for + /// the wire. + /// + /// Compare against ``CloudKitService/maxRecordDataBytes`` (1 MB) to + /// pre-flight CloudKit's per-record data limit before calling + /// ``CloudKitService/modifyRecords(_:atomic:database:)``. Delete operations + /// carry a small envelope (record name, record type, empty fields) so they + /// report a tiny but non-zero size. + /// + /// Asset field values carry only their reference metadata here; the binary + /// blob travels via the CDN and is bounded separately by + /// ``CloudKitService/maxAssetUploadBytes``. + public func encodedRecordSize() throws -> Int { + let apiOperation = try Components.Schemas.RecordOperation(from: self) + guard let record = apiOperation.record else { return 0 } + return try JSONEncoder().encode(record).count + } +} diff --git a/Sources/MistKit/OpenAPI/LoggingMiddleware.swift b/Sources/MistKit/OpenAPI/LoggingMiddleware.swift index 7c6db8f9..a5b2f882 100644 --- a/Sources/MistKit/OpenAPI/LoggingMiddleware.swift +++ b/Sources/MistKit/OpenAPI/LoggingMiddleware.swift @@ -37,6 +37,13 @@ import OpenAPIRuntime /// Emits at `.debug` level — install a `LogHandler` and set /// `logLevel = .debug` on `com.brightdigit.MistKit.middleware` to opt in. internal struct LoggingMiddleware: ClientMiddleware { + /// Maximum bytes of a response body collected at debug level. + /// + /// Sized to surface the JSON envelope and error reason without doubling + /// the memory footprint of large CloudKit responses. Bodies bigger than + /// this still stream through to the caller untouched. + private static let responseBodyLogCap: Int = 64 * 1_024 + private let logger = Logger(subsystem: .middleware) internal func intercept( @@ -92,20 +99,36 @@ internal struct LoggingMiddleware: ClientMiddleware { } #if !os(WASI) - return await logResponseBody(body) + return await logResponseBody(body, contentType: response.headerFields[.contentType]) #else return body #endif } #if !os(WASI) - private func logResponseBody(_ responseBody: HTTPBody?) async -> HTTPBody? { + private func logResponseBody( + _ responseBody: HTTPBody?, + contentType: String? + ) async -> HTTPBody? { guard let responseBody = responseBody else { return nil } + // Only collect bodies we can actually render as text. Asset payloads + // and other binary streams just inflate memory without producing a + // useful log line. + guard let contentType = contentType, + contentType.lowercased().contains("application/json") + else { + logger.debug("📄 Response Body: ") + return responseBody + } + do { - let bodyData = try await Data(collecting: responseBody, upTo: 1_024 * 1_024) + let bodyData = try await Data( + collecting: responseBody, + upTo: Self.responseBodyLogCap + ) logBodyData(bodyData) return HTTPBody(bodyData) } catch { diff --git a/Tests/MistKitTests/CloudKitService/DiscoverUserIdentities/CloudKitServiceTests.DiscoverUserIdentities+InvalidEmail.swift b/Tests/MistKitTests/CloudKitService/DiscoverUserIdentities/CloudKitServiceTests.DiscoverUserIdentities+InvalidEmail.swift index 5516401a..340751fb 100644 --- a/Tests/MistKitTests/CloudKitService/DiscoverUserIdentities/CloudKitServiceTests.DiscoverUserIdentities+InvalidEmail.swift +++ b/Tests/MistKitTests/CloudKitService/DiscoverUserIdentities/CloudKitServiceTests.DiscoverUserIdentities+InvalidEmail.swift @@ -55,11 +55,9 @@ extension CloudKitServiceTests.DiscoverUserIdentities { _ = try await service.discoverUserIdentities(lookupInfos: [lookup]) } throws: { error in guard let ckError = error as? CloudKitError, - case .httpErrorWithDetails(let statusCode, let serverErrorCode, let reason) = ckError + case .badRequest(let reason) = ckError else { return false } - return statusCode == 400 - && serverErrorCode == "BAD_REQUEST" - && reason?.contains("Invalid email") == true + return reason?.contains("Invalid email") == true } } } diff --git a/Tests/MistKitTests/CloudKitService/FetchChanges/CloudKitServiceTests.FetchChanges+Validation.swift b/Tests/MistKitTests/CloudKitService/FetchChanges/CloudKitServiceTests.FetchChanges+Validation.swift index 48c7e02f..20644f91 100644 --- a/Tests/MistKitTests/CloudKitService/FetchChanges/CloudKitServiceTests.FetchChanges+Validation.swift +++ b/Tests/MistKitTests/CloudKitService/FetchChanges/CloudKitServiceTests.FetchChanges+Validation.swift @@ -35,71 +35,6 @@ import Testing extension CloudKitServiceTests.FetchChanges { @Suite("Validation") internal struct Validation { - @Test("fetchRecordChanges() throws 400 for limit of 0") - internal func fetchRecordChangesThrowsForZeroLimit() async throws { - guard #available(macOS 11.0, iOS 14.0, tvOS 14.0, watchOS 7.0, *) else { - Issue.record("CloudKitService is not available on this operating system.") - return - } - let service = try await CloudKitServiceTests.FetchChanges.makeSuccessfulService() - - await #expect { - try await service.fetchRecordChanges( - resultsLimit: 0, - database: .public(.prefers(.serverToServer)) - ) - } throws: { error in - guard let ckError = error as? CloudKitError, - case .httpErrorWithRawResponse(let status, _) = ckError - else { return false } - return status == 400 - } - } - - @Test("fetchRecordChanges() throws 400 for limit over 200") - internal func fetchRecordChangesThrowsForLimitOver200() async throws { - guard #available(macOS 11.0, iOS 14.0, tvOS 14.0, watchOS 7.0, *) else { - Issue.record("CloudKitService is not available on this operating system.") - return - } - let service = try await CloudKitServiceTests.FetchChanges.makeSuccessfulService() - - await #expect { - try await service.fetchRecordChanges( - resultsLimit: 201, - database: .public(.prefers(.serverToServer)) - ) - } throws: { error in - guard let ckError = error as? CloudKitError, - case .httpErrorWithRawResponse(let status, _) = ckError - else { return false } - return status == 400 - } - } - - @Test("fetchRecordChanges() accepts valid limit values") - internal func fetchRecordChangesAcceptsValidLimits() async throws { - guard #available(macOS 11.0, iOS 14.0, tvOS 14.0, watchOS 7.0, *) else { - Issue.record("CloudKitService is not available on this operating system.") - return - } - let service = try await CloudKitServiceTests.FetchChanges.makeSuccessfulService() - - // Minimum valid limit - let result1 = try await service.fetchRecordChanges( - resultsLimit: 1, - database: .public(.prefers(.serverToServer)) - ) - #expect(result1.records.isEmpty == false || result1.syncToken != nil) - - // Maximum valid limit - let result200 = try await service.fetchRecordChanges( - resultsLimit: 200, - database: .public(.prefers(.serverToServer)) - ) - #expect(result200.records.isEmpty == false || result200.syncToken != nil) - } - @Test("fetchAllRecordChanges() throws invalidResponse for moreComing:true with nil syncToken") internal func fetchAllRecordChangesThrowsForNilSyncTokenWithMoreComing() async throws { guard #available(macOS 11.0, iOS 14.0, tvOS 14.0, watchOS 7.0, *) else { diff --git a/Tests/MistKitTests/CloudKitService/FetchZoneChanges/CloudKitServiceTests.FetchZoneChanges+ErrorHandling.swift b/Tests/MistKitTests/CloudKitService/FetchZoneChanges/CloudKitServiceTests.FetchZoneChanges+ErrorHandling.swift index 89cfe880..d3c5369b 100644 --- a/Tests/MistKitTests/CloudKitService/FetchZoneChanges/CloudKitServiceTests.FetchZoneChanges+ErrorHandling.swift +++ b/Tests/MistKitTests/CloudKitService/FetchZoneChanges/CloudKitServiceTests.FetchZoneChanges+ErrorHandling.swift @@ -54,11 +54,9 @@ extension CloudKitServiceTests.FetchZoneChanges { _ = try await service.fetchZoneChanges(syncToken: "garbage-token") } throws: { error in guard let ckError = error as? CloudKitError, - case .httpErrorWithDetails(let statusCode, let serverErrorCode, let reason) = ckError + case .badRequest(let reason) = ckError else { return false } - return statusCode == 400 - && serverErrorCode == "BAD_REQUEST" - && reason?.contains("Invalid syncToken") == true + return reason?.contains("Invalid syncToken") == true } } @@ -83,11 +81,9 @@ extension CloudKitServiceTests.FetchZoneChanges { _ = try await service.fetchZoneChanges(syncToken: "expired-token") } throws: { error in guard let ckError = error as? CloudKitError, - case .httpErrorWithDetails(let statusCode, let serverErrorCode, let reason) = ckError + case .badRequest(let reason) = ckError else { return false } - return statusCode == 400 - && serverErrorCode == "BAD_REQUEST" - && reason?.contains("expired") == true + return reason?.contains("expired") == true } } diff --git a/Tests/MistKitTests/CloudKitService/LookupZones/CloudKitServiceTests.LookupZones+Helpers.swift b/Tests/MistKitTests/CloudKitService/LookupZones/CloudKitServiceTests.LookupZones+Helpers.swift index 7baf2c40..bbccb4eb 100644 --- a/Tests/MistKitTests/CloudKitService/LookupZones/CloudKitServiceTests.LookupZones+Helpers.swift +++ b/Tests/MistKitTests/CloudKitService/LookupZones/CloudKitServiceTests.LookupZones+Helpers.swift @@ -48,6 +48,31 @@ extension CloudKitServiceTests.LookupZones { transport: transport ) } + + internal static func makeServiceReturningZoneWithoutName() async throws -> CloudKitService { + let responseJSON = """ + { + "zones": [ + { "zoneID": { "zoneName": "valid-zone", "ownerName": "_defaultOwner" } }, + { "zoneID": { "ownerName": "_defaultOwner" } } + ] + } + """ + var headers = HTTPFields() + headers[.contentType] = "application/json" + let response = ResponseConfig( + statusCode: 200, + headers: headers, + body: Data(responseJSON.utf8), + error: nil + ) + let transport = MockTransport(responseProvider: ResponseProvider(defaultResponse: response)) + return try CloudKitService( + containerIdentifier: TestConstants.serviceContainerIdentifier, + credentials: Credentials(apiAuth: APICredentials(apiToken: testAPIToken)), + transport: transport + ) + } } // MARK: - LookupZones Response Builders diff --git a/Tests/MistKitTests/CloudKitService/LookupZones/CloudKitServiceTests.LookupZones+SuccessCases.swift b/Tests/MistKitTests/CloudKitService/LookupZones/CloudKitServiceTests.LookupZones+SuccessCases.swift index 8c3e155f..45ac6149 100644 --- a/Tests/MistKitTests/CloudKitService/LookupZones/CloudKitServiceTests.LookupZones+SuccessCases.swift +++ b/Tests/MistKitTests/CloudKitService/LookupZones/CloudKitServiceTests.LookupZones+SuccessCases.swift @@ -90,5 +90,24 @@ extension CloudKitServiceTests.LookupZones { #expect(zones.isEmpty) } + + @Test("lookupZones() drops zones with missing zoneName instead of substituting placeholder") + internal func lookupZonesDropsZonesWithoutName() async throws { + guard #available(macOS 11.0, iOS 14.0, tvOS 14.0, watchOS 7.0, *) else { + Issue.record("CloudKitService is not available on this operating system.") + return + } + let service = + try await CloudKitServiceTests.LookupZones.makeServiceReturningZoneWithoutName() + + let zones = try await service.lookupZones( + zoneIDs: [ZoneID(zoneName: "valid-zone", ownerName: nil)], + database: .public(.prefers(.serverToServer)) + ) + + #expect(zones.count == 1) + #expect(zones.first?.zoneName == "valid-zone") + #expect(!zones.contains { $0.zoneName == "Unknown" }) + } } } diff --git a/Tests/MistKitTests/CloudKitService/LookupZones/CloudKitServiceTests.LookupZones+Validation.swift b/Tests/MistKitTests/CloudKitService/LookupZones/CloudKitServiceTests.LookupZones+Validation.swift deleted file mode 100644 index c39d94aa..00000000 --- a/Tests/MistKitTests/CloudKitService/LookupZones/CloudKitServiceTests.LookupZones+Validation.swift +++ /dev/null @@ -1,96 +0,0 @@ -// -// CloudKitServiceTests.LookupZones+Validation.swift -// MistKit -// -// Created by Leo Dion. -// Copyright © 2026 BrightDigit. -// -// Permission is hereby granted, free of charge, to any person -// obtaining a copy of this software and associated documentation -// files (the "Software"), to deal in the Software without -// restriction, including without limitation the rights to use, -// copy, modify, merge, publish, distribute, sublicense, and/or -// sell copies of the Software, and to permit persons to whom the -// Software is furnished to do so, subject to the following -// conditions: -// -// The above copyright notice and this permission notice shall be -// included in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES -// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT -// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -// OTHER DEALINGS IN THE SOFTWARE. -// - -import Foundation -import Testing - -@testable import MistKit - -extension CloudKitServiceTests.LookupZones { - @Suite("Validation") - internal struct Validation { - @Test("lookupZones() throws 400 for empty zoneIDs array") - internal func lookupZonesThrowsForEmptyZoneIDs() async throws { - guard #available(macOS 11.0, iOS 14.0, tvOS 14.0, watchOS 7.0, *) else { - Issue.record("CloudKitService is not available on this operating system.") - return - } - let service = try await CloudKitServiceTests.LookupZones.makeSuccessfulService() - - await #expect { - try await service.lookupZones(zoneIDs: []) - } throws: { error in - guard let ckError = error as? CloudKitError, - case .httpErrorWithRawResponse(let status, _) = ckError - else { return false } - return status == 400 - } - } - - @Test("lookupZones() throws 400 for zone with empty zoneName") - internal func lookupZonesThrowsForEmptyZoneName() async throws { - guard #available(macOS 11.0, iOS 14.0, tvOS 14.0, watchOS 7.0, *) else { - Issue.record("CloudKitService is not available on this operating system.") - return - } - let service = try await CloudKitServiceTests.LookupZones.makeSuccessfulService() - - await #expect { - try await service.lookupZones(zoneIDs: [ZoneID(zoneName: "", ownerName: nil)]) - } throws: { error in - guard let ckError = error as? CloudKitError, - case .httpErrorWithRawResponse(let status, _) = ckError - else { return false } - return status == 400 - } - } - - @Test("lookupZones() throws 400 when any zone has an empty zoneName") - internal func lookupZonesThrowsForMixedZoneNames() async throws { - guard #available(macOS 11.0, iOS 14.0, tvOS 14.0, watchOS 7.0, *) else { - Issue.record("CloudKitService is not available on this operating system.") - return - } - let service = try await CloudKitServiceTests.LookupZones.makeSuccessfulService() - let zoneIDs = [ - ZoneID(zoneName: "_defaultZone", ownerName: nil), - ZoneID(zoneName: "", ownerName: nil), - ] - - await #expect { - try await service.lookupZones(zoneIDs: zoneIDs) - } throws: { error in - guard let ckError = error as? CloudKitError, - case .httpErrorWithRawResponse(let status, _) = ckError - else { return false } - return status == 400 - } - } - } -} diff --git a/Tests/MistKitTests/CloudKitService/ModifyZones/CloudKitServiceTests.ModifyZones+Validation.swift b/Tests/MistKitTests/CloudKitService/ModifyZones/CloudKitServiceTests.ModifyZones+Validation.swift deleted file mode 100644 index 65efcc11..00000000 --- a/Tests/MistKitTests/CloudKitService/ModifyZones/CloudKitServiceTests.ModifyZones+Validation.swift +++ /dev/null @@ -1,120 +0,0 @@ -// -// CloudKitServiceTests.ModifyZones+Validation.swift -// MistKit -// -// Created by Leo Dion. -// Copyright © 2026 BrightDigit. -// -// Permission is hereby granted, free of charge, to any person -// obtaining a copy of this software and associated documentation -// files (the "Software"), to deal in the Software without -// restriction, including without limitation the rights to use, -// copy, modify, merge, publish, distribute, sublicense, and/or -// sell copies of the Software, and to permit persons to whom the -// Software is furnished to do so, subject to the following -// conditions: -// -// The above copyright notice and this permission notice shall be -// included in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES -// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT -// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -// OTHER DEALINGS IN THE SOFTWARE. -// - -import Foundation -import Testing - -@testable import MistKit - -extension CloudKitServiceTests.ModifyZones { - @Suite("Validation") - internal struct Validation { - @Test("modifyZones() throws 400 for empty operations array") - internal func modifyZonesThrowsForEmptyOperations() async throws { - guard #available(macOS 11.0, iOS 14.0, tvOS 14.0, watchOS 7.0, *) else { - Issue.record("CloudKitService is not available on this operating system.") - return - } - let service = try await CloudKitServiceTests.ModifyZones.makeSuccessfulService() - - await #expect { - try await service.modifyZones([], database: .private) - } throws: { error in - guard let ckError = error as? CloudKitError, - case .httpErrorWithRawResponse(let status, _) = ckError - else { return false } - return status == 400 - } - } - - @Test("modifyZones() throws 400 for operation with empty zoneName") - internal func modifyZonesThrowsForEmptyZoneName() async throws { - guard #available(macOS 11.0, iOS 14.0, tvOS 14.0, watchOS 7.0, *) else { - Issue.record("CloudKitService is not available on this operating system.") - return - } - let service = try await CloudKitServiceTests.ModifyZones.makeSuccessfulService() - - await #expect { - try await service.modifyZones( - [.create(ZoneID(zoneName: "", ownerName: nil))], - database: .private - ) - } throws: { error in - guard let ckError = error as? CloudKitError, - case .httpErrorWithRawResponse(let status, _) = ckError - else { return false } - return status == 400 - } - } - - @Test("modifyZones() throws 400 when any operation has an empty zoneName") - internal func modifyZonesThrowsForMixedZoneNames() async throws { - guard #available(macOS 11.0, iOS 14.0, tvOS 14.0, watchOS 7.0, *) else { - Issue.record("CloudKitService is not available on this operating system.") - return - } - let service = try await CloudKitServiceTests.ModifyZones.makeSuccessfulService() - let operations: [ZoneOperation] = [ - .create(ZoneID(zoneName: "Articles", ownerName: nil)), - .delete(ZoneID(zoneName: "", ownerName: nil)), - ] - - await #expect { - try await service.modifyZones(operations, database: .private) - } throws: { error in - guard let ckError = error as? CloudKitError, - case .httpErrorWithRawResponse(let status, _) = ckError - else { return false } - return status == 400 - } - } - - @Test("modifyZones() throws 400 for .public database") - internal func modifyZonesThrowsForPublicDatabase() async throws { - guard #available(macOS 11.0, iOS 14.0, tvOS 14.0, watchOS 7.0, *) else { - Issue.record("CloudKitService is not available on this operating system.") - return - } - let service = try await CloudKitServiceTests.ModifyZones.makeSuccessfulService() - - await #expect { - try await service.modifyZones( - [.create(ZoneID(zoneName: "Articles", ownerName: nil))], - database: .public(.prefers(.serverToServer)) - ) - } throws: { error in - guard let ckError = error as? CloudKitError, - case .httpErrorWithRawResponse(let status, _) = ckError - else { return false } - return status == 400 - } - } - } -} diff --git a/Tests/MistKitTests/CloudKitService/Query/CloudKitServiceTests.Query+Helpers.swift b/Tests/MistKitTests/CloudKitService/Query/CloudKitServiceTests.Query+Helpers.swift index 7d4163b6..47a76773 100644 --- a/Tests/MistKitTests/CloudKitService/Query/CloudKitServiceTests.Query+Helpers.swift +++ b/Tests/MistKitTests/CloudKitService/Query/CloudKitServiceTests.Query+Helpers.swift @@ -33,20 +33,6 @@ import Testing @testable import MistKit extension CloudKitServiceTests.Query { - /// Create service for validation error testing - internal static func makeValidationErrorService( - _ errorType: ValidationErrorType - ) throws -> CloudKitService { - let transport = MockTransport( - responseProvider: .validationError(errorType) - ) - return try CloudKitService( - containerIdentifier: TestConstants.serviceContainerIdentifier, - credentials: Credentials(apiAuth: APICredentials(apiToken: "test-token")), - transport: transport - ) - } - /// Create service for successful operations internal static func makeSuccessfulService() throws -> CloudKitService { let transport = MockTransport( diff --git a/Tests/MistKitTests/CloudKitService/Query/CloudKitServiceTests.Query+Validation.swift b/Tests/MistKitTests/CloudKitService/Query/CloudKitServiceTests.Query+Validation.swift deleted file mode 100644 index c8453bd6..00000000 --- a/Tests/MistKitTests/CloudKitService/Query/CloudKitServiceTests.Query+Validation.swift +++ /dev/null @@ -1,142 +0,0 @@ -// -// CloudKitServiceTests.Query+Validation.swift -// MistKit -// -// Created by Leo Dion. -// Copyright © 2026 BrightDigit. -// -// Permission is hereby granted, free of charge, to any person -// obtaining a copy of this software and associated documentation -// files (the "Software"), to deal in the Software without -// restriction, including without limitation the rights to use, -// copy, modify, merge, publish, distribute, sublicense, and/or -// sell copies of the Software, and to permit persons to whom the -// Software is furnished to do so, subject to the following -// conditions: -// -// The above copyright notice and this permission notice shall be -// included in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES -// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT -// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -// OTHER DEALINGS IN THE SOFTWARE. -// - -import Foundation -import Testing - -@testable import MistKit - -extension CloudKitServiceTests.Query { - @Suite("Validation") - internal struct Validation { - @Test("queryRecords() validates empty recordType") - internal func queryRecordsValidatesEmptyRecordType() async throws { - guard #available(macOS 11.0, iOS 14.0, tvOS 14.0, watchOS 7.0, *) else { - Issue.record("CloudKitService is not available on this operating system.") - return - } - let service = try CloudKitServiceTests.Query.makeValidationErrorService(.emptyRecordType) - - do { - _ = try await service.queryRecords( - recordType: "", - database: .public(.prefers(.serverToServer)) - ) - Issue.record("Expected error for empty recordType") - } catch let error as CloudKitError { - // Verify we get the correct validation error - if case .httpErrorWithRawResponse(let statusCode, let response) = error { - #expect(statusCode == 400) - #expect(response.contains("recordType cannot be empty")) - } else { - Issue.record("Expected httpErrorWithRawResponse error") - } - } catch { - Issue.record("Expected CloudKitError, got \(type(of: error))") - } - } - - @Test("queryRecords() validates limit too small", arguments: [-1, 0]) - internal func queryRecordsValidatesLimitTooSmall(limit: Int) async throws { - guard #available(macOS 11.0, iOS 14.0, tvOS 14.0, watchOS 7.0, *) else { - Issue.record("CloudKitService is not available on this operating system.") - return - } - let service = try CloudKitServiceTests.Query.makeValidationErrorService(.limitTooSmall(limit)) - - do { - _ = try await service.queryRecords( - recordType: "Article", - limit: limit, - database: .public(.prefers(.serverToServer)) - ) - Issue.record("Expected error for limit \(limit)") - } catch { - if case .httpErrorWithRawResponse(let statusCode, let response) = error { - #expect(statusCode == 400) - #expect(response.contains("limit must be between 1 and 200")) - } else { - Issue.record("Expected httpErrorWithRawResponse error") - } - } - } - - @Test("queryRecords() validates limit too large", arguments: [201, 300, 1_000]) - internal func queryRecordsValidatesLimitTooLarge(limit: Int) async throws { - guard #available(macOS 11.0, iOS 14.0, tvOS 14.0, watchOS 7.0, *) else { - Issue.record("CloudKitService is not available on this operating system.") - return - } - let service = try CloudKitServiceTests.Query.makeValidationErrorService(.limitTooLarge(limit)) - - do { - _ = try await service.queryRecords( - recordType: "Article", - limit: limit, - database: .public(.prefers(.serverToServer)) - ) - Issue.record("Expected error for limit \(limit)") - } catch { - if case .httpErrorWithRawResponse(let statusCode, let response) = error { - #expect(statusCode == 400) - #expect(response.contains("limit must be between 1 and 200")) - } else { - Issue.record("Expected httpErrorWithRawResponse error") - } - } - } - - @Test("queryRecords() accepts valid limit range", arguments: [1, 50, 100, 200]) - internal func queryRecordsAcceptsValidLimitRange(limit: Int) async throws { - guard #available(macOS 11.0, iOS 14.0, tvOS 14.0, watchOS 7.0, *) else { - Issue.record("CloudKitService is not available on this operating system.") - return - } - let service = try CloudKitServiceTests.Query.makeSuccessfulService() - - // This test verifies validation passes - actual API call will fail without real credentials - // but we're testing that validation doesn't throw - do { - _ = try await service.queryRecords( - recordType: "Article", - limit: limit, - database: .public(.prefers(.serverToServer)) - ) - Issue.record("Expected network error since we don't have real credentials") - } catch { - // We expect a network/auth error, not a validation error - // Validation errors have status code 400 - if case .httpErrorWithRawResponse(let statusCode, _) = error { - #expect(statusCode != 400, "Validation should not fail for limit \(limit)") - } - // Other CloudKit errors are expected (auth, network, etc.) - } - } - } -} diff --git a/Tests/MistKitTests/CloudKitService/SizeLimits/CloudKitServiceTests.SizeLimits+Assets.swift b/Tests/MistKitTests/CloudKitService/SizeLimits/CloudKitServiceTests.SizeLimits+Assets.swift new file mode 100644 index 00000000..a08de99e --- /dev/null +++ b/Tests/MistKitTests/CloudKitService/SizeLimits/CloudKitServiceTests.SizeLimits+Assets.swift @@ -0,0 +1,111 @@ +// +// CloudKitServiceTests.SizeLimits+Assets.swift +// MistKit +// +// Created by Leo Dion. +// Copyright © 2026 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +import Foundation +import Testing + +@testable import MistKit + +extension CloudKitServiceTests.SizeLimits { + @Suite("Assets") + internal struct Assets { + private static func makeService() throws -> CloudKitService { + let provider = ResponseProvider(defaultResponse: .success()) + return try CloudKitService( + containerIdentifier: TestConstants.serviceContainerIdentifier, + credentials: Credentials(apiAuth: APICredentials(apiToken: TestConstants.apiToken)), + transport: MockTransport(responseProvider: provider) + ) + } + + /// Uploader that always returns the given (status, body) so we can + /// simulate a CDN-side 413 without involving the network. + private static func failingUploader( + status: Int, + body: Data = Data() + ) -> AssetUploader { + { _, _ in (status, body) } + } + + @Test( + "uploadAssetData enriches 413 with assetExceedsSizeLimit when data is over 15 MB", + .disabled(if: Platform.isWasm)) + internal func assetHintAttachedOnOversizedData() async throws { + guard #available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *) else { + Issue.record("CloudKitService is not available on this operating system.") + return + } + let service = try Self.makeService() + let oversized = Data(count: CloudKitService.maxAssetUploadBytes + 1) + let url = try #require(URL(string: "https://cvws.icloud-content.com/test")) + + await #expect { + _ = try await service.uploadAssetData( + oversized, + to: url, + using: Self.failingUploader(status: 413) + ) + } throws: { error in + guard let ckError = error as? CloudKitError, + case .quotaExceeded(_, let hint) = ckError, + case .assetExceedsSizeLimit(let bytes, let max) = hint + else { return false } + return bytes == oversized.count && max == CloudKitService.maxAssetUploadBytes + } + } + + @Test( + "uploadAssetData passes 413 through unenriched when data is within the 15 MB limit", + .disabled(if: Platform.isWasm)) + internal func assetHintNilWhenDataSmall() async throws { + guard #available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *) else { + Issue.record("CloudKitService is not available on this operating system.") + return + } + let service = try Self.makeService() + let withinLimit = Data(count: 1_024) + let url = try #require(URL(string: "https://cvws.icloud-content.com/test")) + + await #expect { + _ = try await service.uploadAssetData( + withinLimit, + to: url, + using: Self.failingUploader(status: 413) + ) + } throws: { error in + guard let ckError = error as? CloudKitError else { return false } + // No quota hint should be attached — the bare 413 propagates as-is. + if case .quotaExceeded(_, let hint) = ckError, hint != nil { + return false + } + return ckError.httpStatusCode == 413 + } + } + } +} diff --git a/Tests/MistKitTests/CloudKitService/SizeLimits/CloudKitServiceTests.SizeLimits+Records.swift b/Tests/MistKitTests/CloudKitService/SizeLimits/CloudKitServiceTests.SizeLimits+Records.swift new file mode 100644 index 00000000..a59f48ed --- /dev/null +++ b/Tests/MistKitTests/CloudKitService/SizeLimits/CloudKitServiceTests.SizeLimits+Records.swift @@ -0,0 +1,109 @@ +// +// CloudKitServiceTests.SizeLimits+Records.swift +// MistKit +// +// Created by Leo Dion. +// Copyright © 2026 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +import Foundation +import Testing + +@testable import MistKit + +extension CloudKitServiceTests.SizeLimits { + @Suite("Records") + internal struct Records { + private static func makeService( + statusCode: Int = 413, + serverErrorCode: String = "QUOTA_EXCEEDED", + reason: String = "Record exceeds maximum size" + ) throws -> CloudKitService { + let provider = ResponseProvider( + defaultResponse: .cloudKitError( + statusCode: statusCode, + serverErrorCode: serverErrorCode, + reason: reason + ) + ) + return try CloudKitService( + containerIdentifier: TestConstants.serviceContainerIdentifier, + credentials: Credentials(apiAuth: APICredentials(apiToken: TestConstants.apiToken)), + transport: MockTransport(responseProvider: provider) + ) + } + + @Test("modifyRecords enriches QUOTA_EXCEEDED with recordExceedsSizeLimit hint") + internal func recordHintAttachedOnOversizedBatch() async throws { + let service = try Self.makeService() + + // 1.5 MB of string content — easily exceeds CloudKit's 1 MB per-record cap. + let oversized = String(repeating: "x", count: 1_500_000) + let smallOperation = RecordOperation.create( + recordType: "Note", + fields: ["body": .string("small")] + ) + let oversizedOperation = RecordOperation.create( + recordType: "Note", + fields: ["body": .string(oversized)] + ) + + await #expect { + _ = try await service.modifyRecords( + [smallOperation, oversizedOperation], + database: .public(.prefers(.serverToServer)) + ) + } throws: { error in + guard let ckError = error as? CloudKitError, + case .quotaExceeded(_, let hint) = ckError, + case .recordExceedsSizeLimit(let index, let bytes, let max) = hint + else { return false } + return index == 1 + && bytes > CloudKitService.maxRecordDataBytes + && max == CloudKitService.maxRecordDataBytes + } + } + + @Test("modifyRecords passes QUOTA_EXCEEDED through unenriched when no record is oversized") + internal func recordHintNilWhenAllRecordsSmall() async throws { + let service = try Self.makeService(reason: "iCloud storage quota exhausted") + let operation = RecordOperation.create( + recordType: "Note", + fields: ["body": .string("small")] + ) + + await #expect { + _ = try await service.modifyRecords( + [operation], + database: .public(.prefers(.serverToServer)) + ) + } throws: { error in + guard let ckError = error as? CloudKitError, + case .quotaExceeded(let reason, let hint) = ckError + else { return false } + return hint == nil && reason?.contains("storage quota") == true + } + } + } +} diff --git a/Tests/MistKitTests/Mocks/ValidationErrorType.swift b/Tests/MistKitTests/CloudKitService/SizeLimits/CloudKitServiceTests.SizeLimits.swift similarity index 83% rename from Tests/MistKitTests/Mocks/ValidationErrorType.swift rename to Tests/MistKitTests/CloudKitService/SizeLimits/CloudKitServiceTests.SizeLimits.swift index 0b7bdfb5..13b07e69 100644 --- a/Tests/MistKitTests/Mocks/ValidationErrorType.swift +++ b/Tests/MistKitTests/CloudKitService/SizeLimits/CloudKitServiceTests.SizeLimits.swift @@ -1,9 +1,9 @@ // -// ValidationErrorType.swift +// CloudKitServiceTests.SizeLimits.swift // MistKit // // Created by Leo Dion. -// Copyright © 2025 BrightDigit. +// Copyright © 2026 BrightDigit. // // Permission is hereby granted, free of charge, to any person // obtaining a copy of this software and associated documentation @@ -27,9 +27,9 @@ // OTHER DEALINGS IN THE SOFTWARE. // -/// Types of validation errors that can occur -internal enum ValidationErrorType: Sendable { - case emptyRecordType - case limitTooSmall(Int) - case limitTooLarge(Int) +import Testing + +extension CloudKitServiceTests { + @Suite("SizeLimits") + internal enum SizeLimits {} } diff --git a/Tests/MistKitTests/CloudKitService/Upload/CloudKitServiceTests.Upload+ErrorHandling.swift b/Tests/MistKitTests/CloudKitService/Upload/CloudKitServiceTests.Upload+ErrorHandling.swift index 56f3641f..807a410a 100644 --- a/Tests/MistKitTests/CloudKitService/Upload/CloudKitServiceTests.Upload+ErrorHandling.swift +++ b/Tests/MistKitTests/CloudKitService/Upload/CloudKitServiceTests.Upload+ErrorHandling.swift @@ -71,10 +71,8 @@ extension CloudKitServiceTests.Upload { Issue.record("CloudKitService is not available on this operating system.") return } - let service = try await CloudKitServiceTests.Upload.makeUploadValidationErrorService( - .emptyData - ) - let testData = Data() // Empty data triggers 400 + let service = try await CloudKitServiceTests.Upload.makeUploadBadRequestService() + let testData = Data(count: 1) do { _ = try await service.uploadAssets( @@ -85,10 +83,10 @@ extension CloudKitServiceTests.Upload { ) Issue.record("Expected bad request error") } catch let error as CloudKitError { - if case .httpErrorWithRawResponse(let statusCode, _) = error { - #expect(statusCode == 400, "Should return 400 Bad Request") + if case .badRequest = error { + // expected } else { - Issue.record("Expected httpErrorWithRawResponse error, got \(error)") + Issue.record("Expected .badRequest error, got \(error)") } } catch { Issue.record("Expected CloudKitError, got \(type(of: error))") diff --git a/Tests/MistKitTests/CloudKitService/Upload/CloudKitServiceTests.Upload+Helpers.swift b/Tests/MistKitTests/CloudKitService/Upload/CloudKitServiceTests.Upload+Helpers.swift index ec23e928..3591f4b8 100644 --- a/Tests/MistKitTests/CloudKitService/Upload/CloudKitServiceTests.Upload+Helpers.swift +++ b/Tests/MistKitTests/CloudKitService/Upload/CloudKitServiceTests.Upload+Helpers.swift @@ -33,12 +33,6 @@ import Testing @testable import MistKit -/// Types of upload validation errors that can occur -internal enum UploadValidationErrorType: Sendable { - case emptyData - case oversizedAsset(Int) -} - extension CloudKitServiceTests.Upload { /// Create service for successful upload operations /// Test API token in 64-character hexadecimal format as required by MistKit validation @@ -77,14 +71,21 @@ extension CloudKitServiceTests.Upload { ) } - /// Create service for validation error testing + /// Create service that responds with a 400 BAD_REQUEST for any call — + /// used to exercise CloudKit's server-side bad-request path on + /// asset-upload operations. @available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *) - internal static func makeUploadValidationErrorService( - _ errorType: UploadValidationErrorType + internal static func makeUploadBadRequestService( + reason: String = "Asset data cannot be empty" ) async throws -> CloudKitService { - let responseProvider = ResponseProvider.uploadValidationError(errorType) - - let transport = MockTransport(responseProvider: responseProvider) + let provider = ResponseProvider( + defaultResponse: .cloudKitError( + statusCode: 400, + serverErrorCode: "BAD_REQUEST", + reason: reason + ) + ) + let transport = MockTransport(responseProvider: provider) return try CloudKitService( containerIdentifier: TestConstants.serviceContainerIdentifier, credentials: Credentials(apiAuth: APICredentials(apiToken: testAPIToken)), @@ -113,12 +114,6 @@ extension ResponseProvider { internal static func successfulUpload(tokenCount: Int = 1) -> ResponseProvider { ResponseProvider(defaultResponse: .successfulUploadResponse(tokenCount: tokenCount)) } - - /// Response provider for upload validation errors - internal static func uploadValidationError(_ type: UploadValidationErrorType) -> ResponseProvider - { - ResponseProvider(defaultResponse: .uploadValidationError(type)) - } } extension ResponseConfig { @@ -179,24 +174,4 @@ extension ResponseConfig { error: nil ) } - - /// Creates an upload validation error response (400 Bad Request) - /// - /// - Parameter type: The type of upload validation error - /// - Returns: ResponseConfig with appropriate validation error message - internal static func uploadValidationError(_ type: UploadValidationErrorType) -> ResponseConfig { - let reason: String - switch type { - case .emptyData: - reason = "Asset data cannot be empty" - case .oversizedAsset(let size): - reason = "Asset size \(size) bytes exceeds maximum allowed size of 262144000 bytes (250 MB)" - } - - return cloudKitError( - statusCode: 400, - serverErrorCode: "BAD_REQUEST", - reason: reason - ) - } } diff --git a/Tests/MistKitTests/CloudKitService/Upload/CloudKitServiceTests.Upload+Validation.swift b/Tests/MistKitTests/CloudKitService/Upload/CloudKitServiceTests.Upload+Validation.swift index f5c1fc63..5d58b446 100644 --- a/Tests/MistKitTests/CloudKitService/Upload/CloudKitServiceTests.Upload+Validation.swift +++ b/Tests/MistKitTests/CloudKitService/Upload/CloudKitServiceTests.Upload+Validation.swift @@ -35,64 +35,6 @@ import Testing extension CloudKitServiceTests.Upload { @Suite("Validation") internal struct Validation { - @Test("uploadAssets() validates empty data") - internal func uploadAssetsValidatesEmptyData() async throws { - guard #available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *) else { - Issue.record("CloudKitService is not available on this operating system.") - return - } - let service = try await CloudKitServiceTests.Upload.makeUploadValidationErrorService( - .emptyData - ) - - do { - _ = try await service.uploadAssets( - data: Data(), - recordType: "Note", - fieldName: "image", - database: .public(.prefers(.serverToServer)) - ) - Issue.record("Expected error for empty data") - } catch { - if case .httpErrorWithRawResponse(let statusCode, let response) = error { - #expect(statusCode == 400) - #expect(response.contains("Asset data cannot be empty")) - } else { - Issue.record("Expected httpErrorWithRawResponse error") - } - } - } - - @Test("uploadAssets() validates 15 MB size limit", .disabled(if: Platform.isWasm)) - internal func uploadAssetsValidates15MBLimit() async throws { - guard #available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *) else { - Issue.record("CloudKitService is not available on this operating system.") - return - } - // Create data just over 15 MB (15 * 1024 * 1024 + 1 bytes) - let oversizedData = Data(count: 15_728_641) - let service = try await CloudKitServiceTests.Upload.makeUploadValidationErrorService( - .oversizedAsset(oversizedData.count) - ) - - do { - _ = try await service.uploadAssets( - data: oversizedData, - recordType: "Note", - fieldName: "image", - database: .public(.prefers(.serverToServer)) - ) - Issue.record("Expected error for oversized asset") - } catch { - if case .httpErrorWithRawResponse(let statusCode, let response) = error { - #expect(statusCode == 413) - #expect(response.contains("exceeds maximum")) - } else { - Issue.record("Expected httpErrorWithRawResponse error, got \(error)") - } - } - } - @Test("uploadAssets() accepts valid data sizes", .disabled(if: Platform.isWasm)) internal func uploadAssetsAcceptsValidSizes() async throws { guard #available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *) else { diff --git a/Tests/MistKitTests/Mocks/ResponseConfig.swift b/Tests/MistKitTests/Mocks/ResponseConfig.swift index 66746228..b27dc047 100644 --- a/Tests/MistKitTests/Mocks/ResponseConfig.swift +++ b/Tests/MistKitTests/Mocks/ResponseConfig.swift @@ -95,25 +95,6 @@ extension ResponseConfig { ) } - /// Creates a validation error response (400 Bad Request) - internal static func validationError(_ type: ValidationErrorType) -> ResponseConfig { - let reason: String - switch type { - case .emptyRecordType: - reason = "recordType cannot be empty" - case .limitTooSmall(let limit): - reason = "limit must be between 1 and 200, got \(limit)" - case .limitTooLarge(let limit): - reason = "limit must be between 1 and 200, got \(limit)" - } - - return cloudKitError( - statusCode: 400, - serverErrorCode: "BAD_REQUEST", - reason: reason - ) - } - /// Creates an authentication error response (401 Unauthorized) internal static func authenticationError() -> ResponseConfig { cloudKitError( diff --git a/Tests/MistKitTests/Mocks/ResponseProvider.swift b/Tests/MistKitTests/Mocks/ResponseProvider.swift index db8e7ac9..c6f9c88f 100644 --- a/Tests/MistKitTests/Mocks/ResponseProvider.swift +++ b/Tests/MistKitTests/Mocks/ResponseProvider.swift @@ -57,11 +57,6 @@ internal actor ResponseProvider { // MARK: - Factory Methods - /// Response provider for validation errors - internal static func validationError(_ type: ValidationErrorType) -> ResponseProvider { - ResponseProvider(defaultResponse: .validationError(type)) - } - /// Response provider for authentication errors internal static func authenticationError() -> ResponseProvider { ResponseProvider(defaultResponse: .authenticationError()) diff --git a/Tests/MistKitTests/Models/RecordOperationEncodedSizeTests.swift b/Tests/MistKitTests/Models/RecordOperationEncodedSizeTests.swift new file mode 100644 index 00000000..faecc3dd --- /dev/null +++ b/Tests/MistKitTests/Models/RecordOperationEncodedSizeTests.swift @@ -0,0 +1,82 @@ +// +// RecordOperationEncodedSizeTests.swift +// MistKit +// +// Created by Leo Dion. +// Copyright © 2026 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +import Foundation +import Testing + +@testable import MistKit + +@Suite("RecordOperation.encodedRecordSize()") +internal struct RecordOperationEncodedSizeTests { + @Test("delete operations report a small envelope-only size") + internal func deleteReportsSmallSize() throws { + let operation = RecordOperation.delete(recordType: "Note", recordName: "n-1") + let size = try operation.encodedRecordSize() + #expect(size > 0) + // Envelope only: recordName + recordType + empty fields object. + #expect(size < 200) + } + + @Test("create operation reports a non-zero size") + internal func createReturnsPositive() throws { + let operation = RecordOperation.create( + recordType: "Note", + fields: ["body": .string("hello")] + ) + #expect(try operation.encodedRecordSize() > 0) + } + + @Test("size scales with field content length") + internal func sizeScalesWithContent() throws { + let small = RecordOperation.create( + recordType: "Note", + fields: ["body": .string("x")] + ) + let large = RecordOperation.create( + recordType: "Note", + fields: ["body": .string(String(repeating: "x", count: 10_000))] + ) + let smallSize = try small.encodedRecordSize() + let largeSize = try large.encodedRecordSize() + #expect(largeSize > smallSize) + // 10_000 chars of "x" plus JSON overhead — must be much larger than the + // single-char baseline. + #expect(largeSize - smallSize >= 9_000) + } + + @Test("size can be compared against CloudKitService.maxRecordDataBytes") + internal func boundaryComparisonCompiles() throws { + let operation = RecordOperation.create( + recordType: "Note", + fields: ["body": .string("ok")] + ) + let size = try operation.encodedRecordSize() + #expect(size <= CloudKitService.maxRecordDataBytes) + } +} diff --git a/Tests/MistKitTests/OpenAPI/LoggingMiddleware/LoggingMiddlewareTests+BodyHandling.swift b/Tests/MistKitTests/OpenAPI/LoggingMiddleware/LoggingMiddlewareTests+BodyHandling.swift new file mode 100644 index 00000000..098b19f2 --- /dev/null +++ b/Tests/MistKitTests/OpenAPI/LoggingMiddleware/LoggingMiddlewareTests+BodyHandling.swift @@ -0,0 +1,106 @@ +// +// LoggingMiddlewareTests+BodyHandling.swift +// MistKit +// +// Created by Leo Dion. +// Copyright © 2026 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +import Foundation +import HTTPTypes +import OpenAPIRuntime +import Testing + +@testable import MistKit + +extension LoggingMiddlewareTests { + @Suite("Body Handling") + internal struct BodyHandling { + @Test("preserves JSON response body") + internal func preservesJSONResponseBody() async throws { + let middleware = LoggingMiddleware() + let request = HTTPRequest( + method: .get, + scheme: "https", + authority: "api.apple-cloudkit.com", + path: "/test" + ) + let baseURL = try #require(URL(string: "https://api.apple-cloudkit.com")) + let jsonData = Data(#"{"foo":"bar"}"#.utf8) + let responseBody = HTTPBody(jsonData) + + let next: + (HTTPRequest, HTTPBody?, URL) async throws + -> (HTTPResponse, HTTPBody?) = { _, _, _ in + var response = HTTPResponse(status: .ok) + response.headerFields[.contentType] = "application/json" + return (response, responseBody) + } + + let (_, returnedBody) = try await middleware.intercept( + request, + body: nil, + baseURL: baseURL, + operationID: "test", + next: next + ) + + let collected = try await Data(collecting: try #require(returnedBody), upTo: .max) + #expect(collected == jsonData) + } + + @Test("preserves non-JSON response body") + internal func preservesNonJSONResponseBody() async throws { + let middleware = LoggingMiddleware() + let request = HTTPRequest( + method: .get, + scheme: "https", + authority: "api.apple-cloudkit.com", + path: "/asset" + ) + let baseURL = try #require(URL(string: "https://api.apple-cloudkit.com")) + let binaryData = Data(repeating: 0xAB, count: 200_000) + let responseBody = HTTPBody(binaryData) + + let next: + (HTTPRequest, HTTPBody?, URL) async throws + -> (HTTPResponse, HTTPBody?) = { _, _, _ in + var response = HTTPResponse(status: .ok) + response.headerFields[.contentType] = "application/octet-stream" + return (response, responseBody) + } + + let (_, returnedBody) = try await middleware.intercept( + request, + body: nil, + baseURL: baseURL, + operationID: "test", + next: next + ) + + let collected = try await Data(collecting: try #require(returnedBody), upTo: .max) + #expect(collected == binaryData) + } + } +} From c0793bb2166b4e76f387a89d99bddf530729fdad Mon Sep 17 00:00:00 2001 From: Leo Dion Date: Mon, 18 May 2026 15:04:27 +0100 Subject: [PATCH 03/35] Docs: error handling, configuration, records, and limits articles MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds four DocC articles to close out the open documentation gaps tracked by parent issue #361: - HandlingErrors.md — three-layer error model (construction, token, request) with retry/recovery guidance. - ConfiguringMistKit.md — container/environment/transport/logging inputs; defers credential construction to AuthenticationAndDatabases. - WorkingWithRecords.md — CRUD, batch, lookup, and sync-via-token walks. - CloudKitLimitsAndPerformance.md — pagination guard, batch sizing, asset upload transport split, rate limits. Also adds inline `# Example` blocks to createRecord, updateRecord (CloudKitService+WriteOperations.swift) and lookupRecords (CloudKitService+LookupOperations.swift) to match the existing example style on queryRecords. modifyRecords and deleteRecord examples live in the WorkingWithRecords article rather than inline to keep the write- operations file under the file_length cap. Updates Documentation.md Topics to surface the new articles and adds an Error Handling group covering the typed error and reason enums. Migration guide (mentioned in #160) is deferred — MistKit is at 1.0.0-beta.x with no stable predecessor; that article should land alongside a future release transition. Closes #115 Closes #116 Closes #160 Co-Authored-By: Claude Opus 4.7 (1M context) --- .../CloudKitService+LookupOperations.swift | 18 ++ .../CloudKitService+WriteOperations.swift | 20 ++ .../CloudKitLimitsAndPerformance.md | 130 ++++++++++++ .../Documentation.docc/ConfiguringMistKit.md | 163 +++++++++++++++ .../Documentation.docc/Documentation.md | 16 ++ .../Documentation.docc/HandlingErrors.md | 183 +++++++++++++++++ .../Documentation.docc/WorkingWithRecords.md | 192 ++++++++++++++++++ 7 files changed, 722 insertions(+) create mode 100644 Sources/MistKit/Documentation.docc/CloudKitLimitsAndPerformance.md create mode 100644 Sources/MistKit/Documentation.docc/ConfiguringMistKit.md create mode 100644 Sources/MistKit/Documentation.docc/HandlingErrors.md create mode 100644 Sources/MistKit/Documentation.docc/WorkingWithRecords.md diff --git a/Sources/MistKit/CloudKitService/CloudKitService+LookupOperations.swift b/Sources/MistKit/CloudKitService/CloudKitService+LookupOperations.swift index 842869e9..4996654e 100644 --- a/Sources/MistKit/CloudKitService/CloudKitService+LookupOperations.swift +++ b/Sources/MistKit/CloudKitService/CloudKitService+LookupOperations.swift @@ -32,6 +32,24 @@ internal import MistKitOpenAPI extension CloudKitService { /// Lookup records by record names + /// - Parameters: + /// - recordNames: Record names to fetch + /// - desiredKeys: Optional array of field names to fetch + /// - database: The CloudKit database scope to read from (`.public`, `.private`, `.shared`) + /// - Returns: Array of RecordInfo for the matched records + /// - Throws: CloudKitError if the operation fails + /// + /// # Example: Bulk lookup with field projection + /// ```swift + /// let articles = try await service.lookupRecords( + /// recordNames: ["article-001", "article-002", "article-003"], + /// desiredKeys: ["title", "publishedDate"], + /// database: .private + /// ) + /// ``` + /// + /// - Note: Pass `desiredKeys` to limit which fields come back. Useful + /// for list views that only need a projection. public func lookupRecords( recordNames: [String], desiredKeys: [String]? = nil, diff --git a/Sources/MistKit/CloudKitService/CloudKitService+WriteOperations.swift b/Sources/MistKit/CloudKitService/CloudKitService+WriteOperations.swift index 5708b937..6fd2e864 100644 --- a/Sources/MistKit/CloudKitService/CloudKitService+WriteOperations.swift +++ b/Sources/MistKit/CloudKitService/CloudKitService+WriteOperations.swift @@ -127,6 +127,15 @@ extension CloudKitService { /// - database: The CloudKit database scope to write to (`.public`, `.private`, `.shared`) /// - Returns: RecordInfo for the created record /// - Throws: CloudKitError if the operation fails + /// + /// # Example + /// ```swift + /// let article = try await service.createRecord( + /// recordType: "Article", + /// fields: ["title": .string("Hello, CloudKit")], + /// database: .private + /// ) + /// ``` public func createRecord( recordType: String, recordName: String? = nil, @@ -155,6 +164,17 @@ extension CloudKitService { /// - database: The CloudKit database scope to write to (`.public`, `.private`, `.shared`) /// - Returns: RecordInfo for the updated record /// - Throws: CloudKitError if the operation fails + /// + /// # Example + /// ```swift + /// let updated = try await service.updateRecord( + /// recordType: "Article", + /// recordName: existing.recordName, + /// fields: ["title": .string("Renamed")], + /// recordChangeTag: existing.recordChangeTag, + /// database: .private + /// ) + /// ``` public func updateRecord( recordType: String, recordName: String, diff --git a/Sources/MistKit/Documentation.docc/CloudKitLimitsAndPerformance.md b/Sources/MistKit/Documentation.docc/CloudKitLimitsAndPerformance.md new file mode 100644 index 00000000..abe55f6c --- /dev/null +++ b/Sources/MistKit/Documentation.docc/CloudKitLimitsAndPerformance.md @@ -0,0 +1,130 @@ +# CloudKit Limits and Performance + +CloudKit caps per-request work and per-account throughput. This article documents the limits MistKit enforces itself, the limits CloudKit enforces server-side, and the design choices in MistKit that shape performance. + +## Overview + +CloudKit Web Services is a remote API with per-request size limits and per-account rate limits. MistKit adds two small guards of its own — a page cap on auto-pagination and a transport-pool separation for asset uploads — and otherwise defers to CloudKit's documented limits. + +| Concern | Enforced where | Notes | +| --- | --- | --- | +| Records per query response | CloudKit | Max 200; the `limit` parameter is validated 1–200. | +| Pages per auto-paginated query | MistKit | `maxPages: 1_000` on ``CloudKitService/queryAllRecords(recordType:filters:sortBy:pageSize:desiredKeys:maxPages:database:)``. | +| Records per modify batch | CloudKit | Practical cap around 200; chunk larger batches client-side. | +| Asset upload size / connection pool | MistKit (transport separation) | `URLSession.shared` used for CDN uploads to avoid HTTP/2 reuse with the API host. | +| Requests per second | CloudKit | Server-side rate limit; surfaces as 503/429. | + +## Pagination guard + +``CloudKitService/queryAllRecords(recordType:filters:sortBy:pageSize:desiredKeys:maxPages:database:)`` walks CloudKit's `continuationMarker` for you. Two safeguards prevent runaway iteration: + +1. **`maxPages` cap (default `1_000`)** — if the auto-paginator hits the cap, it throws ``CloudKitError/paginationLimitExceeded(maxPages:records:)``. The records collected so far are attached to the error so the caller can resume from a narrowed query or accept a partial result. +2. **Stuck-marker detection** — if CloudKit returns an empty page with the same continuation marker it just gave you, the paginator stops cleanly rather than spinning. This guards against a server-side bug pattern where the cursor never advances. + +```swift +do { + let all = try await service.queryAllRecords( + recordType: "AuditEvent", + pageSize: 200, + database: .private + ) +} catch CloudKitError.paginationLimitExceeded(let cap, let partial) { + // Either accept partial, raise `maxPages`, or narrow the query with filters. +} +``` + +Raise `maxPages` when you know the result set is genuinely large. Narrow filters when you don't — most production queries return well under 1,000 pages. + +## Batching writes + +CloudKit's `/records/modify` endpoint accepts a batch of operations in a single round-trip. The practical server-side cap is around 200 operations per request. ``CloudKitService/modifyRecords(_:atomic:database:)`` does not chunk for you — split larger batches yourself: + +```swift +let chunked = stride(from: 0, to: operations.count, by: 200).map { + Array(operations[$0.. Warning: CloudKit's API host (`api.apple-cloudkit.com`) and asset CDN (`cvws.icloud-content.com`) are different origins. Reusing the same HTTP/2 connection across both produces 421 Misdirected Request errors. Asset uploads keep a separate connection pool to avoid this. + +This has two implications for consumers: + +1. **Custom transports do not see asset upload bytes.** If you instrumented `transport:` to log requests, asset uploads will be missing. +2. **A custom ``AssetUploader`` must keep its connection pool separate from the API host.** The default implementation uses `URLSession.shared`; only override it for testing or specialized CDN configurations, and never share an HTTP/2 connection with `api.apple-cloudkit.com`. + +```swift +// Default: production-safe, separate connection pool. +let receipt = try await service.uploadAssets( + data: imageData, + recordType: "Photo", + fieldName: "image", + database: .private +) + +// Testing: inject a mock uploader. +let receipt = try await service.uploadAssets( + data: imageData, + recordType: "Photo", + fieldName: "image", + using: { data, url in (statusCode: 200, data: Data()) }, + database: .private +) +``` + +CloudKit imposes a per-asset size cap (in the tens of megabytes, exact figure documented in [CloudKit Web Services](https://developer.apple.com/documentation/cloudkitwebservices)). Oversized uploads surface as ``CloudKitError/httpErrorWithDetails(statusCode:serverErrorCode:reason:)`` from the CDN. + +## Rate limiting + +CloudKit enforces per-account and per-container rate limits server-side. The exact thresholds vary by account tier and are documented by Apple — MistKit does not duplicate the canonical numbers. Surfaces are: + +- HTTP 503 with retry-after — back off and retry. +- HTTP 429 — back off and retry. +- HTTP 5xx generally — treat as transient. + +See for the retry helper pattern. Read ``CloudKitError/httpStatusCode`` to branch on status without switching on every `httpError*` case. + +> Tip: Hammering the API from a hot loop (sync jobs, migrations) is the usual way to trip rate limits. Throttle long-running tasks to a few requests per second per container, and concentrate concurrency in `modifyRecords` batches rather than in many parallel single-record calls. + +## Connection reuse for the API host + +By default ``CloudKitService`` uses `URLSessionTransport` from `swift-openapi-urlsession`, which gives you HTTP/2 multiplexing against `api.apple-cloudkit.com` automatically. There is no per-call session — every operation through one ``CloudKitService`` shares the underlying URLSession's connection pool, so a burst of operations does not pay TCP/TLS setup per call. + +For custom transports, prefer one transport per `CloudKitService` and reuse the same `CloudKitService` across calls. Creating a fresh service per request defeats connection reuse. + +## Topics + +### Limits + +- ``CloudKitError/paginationLimitExceeded(maxPages:records:)`` +- ``CloudKitService/queryAllRecords(recordType:filters:sortBy:pageSize:desiredKeys:maxPages:database:)`` +- ``CloudKitService/modifyRecords(_:atomic:database:)`` + +### Asset uploads + +- ``CloudKitService/uploadAssets(data:recordType:fieldName:recordName:using:database:)`` +- ``CloudKitService/requestAssetUploadURL(recordType:fieldName:recordName:database:)`` +- ``CloudKitService/uploadAssetData(_:to:using:)`` +- ``AssetUploader`` + +### See Also + +- +- +- diff --git a/Sources/MistKit/Documentation.docc/ConfiguringMistKit.md b/Sources/MistKit/Documentation.docc/ConfiguringMistKit.md new file mode 100644 index 00000000..0a248a3c --- /dev/null +++ b/Sources/MistKit/Documentation.docc/ConfiguringMistKit.md @@ -0,0 +1,163 @@ +# Configuring MistKit + +There is no single `MistKitConfiguration` type — configuration is what you pass to ``CloudKitService``: a container identifier, an ``Environment``, ``Credentials``, and (optionally) a custom transport. + +## Overview + +A configured MistKit setup is one call: + +```swift +let service = CloudKitService( + containerIdentifier: "iCloud.com.example.MyApp", + credentials: credentials, + environment: .production +) +``` + +Everything else — which ``Database`` to use, which signing method on the public database, which token to refresh — is decided per call. This article covers the construction-time inputs (container, environment, transport, logging). For credentials and per-call database selection, see . + +## Container identifier + +The container identifier is the iCloud container your records live in. It is the same string you see in the CloudKit Dashboard under **Container ID**, prefixed with `iCloud.`: + +```swift +"iCloud.com.example.MyApp" +``` + +A single container has separate `development` and `production` schemas, separate record stores, and separate user data. You do not switch containers between environments — you switch ``Environment``. + +> Tip: Containers are configured in the [CloudKit Dashboard](https://icloud.developer.apple.com). The container identifier is also visible in your Xcode app target's CloudKit capability. + +## Environment selection + +``Environment`` has two cases: ``Environment/development`` and ``Environment/production``. They map to distinct CloudKit schemas and stores in the same container: + +| Environment | Used for | +| --- | --- | +| `.development` | Schema migrations, local testing, staging deploys | +| `.production` | Released apps, production backends | + +Drive the choice from configuration, not source: + +```swift +let environment: Environment = ProcessInfo.processInfo + .environment["CLOUDKIT_ENVIRONMENT"] + .flatMap(Environment.init(caseInsensitive:)) + ?? .development +``` + +``Environment/init(caseInsensitive:)`` accepts `"development"` / `"production"` regardless of letter case and returns `nil` on anything else, so a misspelled env var fails closed at startup rather than silently shipping a dev build to prod. + +> Warning: CloudKit promotes schema from `development` to `production` explicitly via the Dashboard. Code referencing fields that exist only in dev will succeed against `.development` and fail against `.production` with ``CloudKitError/httpErrorWithDetails(statusCode:serverErrorCode:reason:)``. + +## Database scope at configuration time + +``CloudKitService`` itself is database-agnostic — there is no `database:` parameter on the initializer. You pick the scope at each call site: + +```swift +try await service.queryRecords(recordType: "Note", database: .private) +try await service.createRecord( + recordType: "FeaturedPost", + fields: fields, + database: .public(.requires(.serverToServer)) +) +``` + +The configuration question for your app is: which credentials does the deployment need to populate so the call sites it makes are reachable? An app that only queries the private database needs a web-auth token. An app that also seeds the public database with server-attributed writes needs server-to-server credentials. An app that does both populates both fields on ``Credentials``. See for the full credential/database matrix. + +## Custom transport + +The default public initializer uses `URLSessionTransport` from `swift-openapi-urlsession`. For testing, request inspection, or platforms without URLSession, supply your own ``ClientTransport`` via the generic initializer: + +```swift +let service = CloudKitService( + containerIdentifier: container, + credentials: credentials, + environment: .development, + transport: customTransport +) +``` + +Common reasons to override the transport: + +- **Tests** — substitute a mock transport that asserts on outgoing requests and returns canned responses. +- **Instrumentation** — wrap `URLSessionTransport` to record request/response pairs for debugging. +- **WASI** — `URLSession` is unavailable, so the WASI build path requires a transport you provide (the public URLSession initializer is gated behind `#if !os(WASI)`). + +> Warning: Asset uploads do **not** flow through the configured `transport`. They use `URLSession.shared` directly to avoid HTTP/2 connection reuse between CloudKit's API host and the CDN, which surfaces as 421 Misdirected Request errors. See for the full rationale. + +## Logging + +MistKit uses [swift-log](https://github.com/apple/swift-log). The package emits to four labeled subsystems; consumers install a `LogHandler` and choose verbosity per subsystem. + +| Subsystem label | Use | +| --- | --- | +| `com.brightdigit.MistKit.api` | CloudKit API operations | +| `com.brightdigit.MistKit.auth` | Authentication and token management | +| `com.brightdigit.MistKit.network` | Network errors | +| `com.brightdigit.MistKit.middleware` | HTTP request/response traces (debug only) | + +Bootstrap the logging system once at process start: + +```swift +import Logging + +LoggingSystem.bootstrap(StreamLogHandler.standardOutput) +``` + +To raise the middleware subsystem to `.debug` for troubleshooting without flooding the rest: + +```swift +LoggingSystem.bootstrap { label in + var handler = StreamLogHandler.standardOutput(label: label) + if label == "com.brightdigit.MistKit.middleware" { + handler.logLevel = .debug + } + return handler +} +``` + +> Note: Protocol traces — request/response bodies, headers, query params — are only emitted at `.debug`. The middleware guards expensive work (1 MiB body collection, query-param parsing) behind a level check, so the default `.info` level pays no overhead. + +There is no built-in redaction. Sensitive data (tokens, raw bodies) appears only at `.debug`; control exposure by leaving the middleware subsystem at `.info` or higher in production. + +## Environment-variable patterns + +MistKit doesn't prescribe a secrets store, but the conventional env-var names line up with what's documented in and used by the in-repo `MistDemo` integration runner: + +| Variable | Used for | +| --- | --- | +| `CLOUDKIT_CONTAINER` | Container identifier | +| `CLOUDKIT_ENVIRONMENT` | `development` or `production` | +| `CLOUDKIT_API_TOKEN` | API token (public database read, web-auth attribution) | +| `CLOUDKIT_WEB_AUTH_TOKEN` | Web-auth token (private/shared, user-identity routes) | +| `CLOUDKIT_KEY_ID` | Server-to-server key ID | +| `CLOUDKIT_PRIVATE_KEY` / `CLOUDKIT_PRIVATE_KEY_PATH` | ECDSA P-256 PEM material (inline or file) | + +A small helper keeps call sites tidy and fails closed on missing values: + +```swift +func env(_ key: String) throws -> String { + guard let value = ProcessInfo.processInfo.environment[key], + !value.isEmpty else { + throw ConfigError.missing(key) + } + return value +} +``` + +> Tip: Validate every required variable at startup, before constructing ``CloudKitService``. Missing or malformed credentials surface as ``CloudKitError/missingCredentials(database:availability:reason:)`` at the first call that needs them — much later than you want to discover the deployment is misconfigured. + +## Topics + +### Configuration + +- ``CloudKitService`` +- ``Environment`` +- ``Database`` + +### See Also + +- +- +- diff --git a/Sources/MistKit/Documentation.docc/Documentation.md b/Sources/MistKit/Documentation.docc/Documentation.md index e2601ee6..b9f96399 100644 --- a/Sources/MistKit/Documentation.docc/Documentation.md +++ b/Sources/MistKit/Documentation.docc/Documentation.md @@ -76,6 +76,9 @@ MistKit runs on macOS, iOS, tvOS, watchOS, visionOS, Linux, WASI, and Windows. S ### Getting Started - +- +- +- - ``CloudKitService`` - ``Database`` - ``Environment`` @@ -130,6 +133,19 @@ MistKit runs on macOS, iOS, tvOS, watchOS, visionOS, Linux, WASI, and Windows. S - ``TokenManagerError`` - ``TokenStorageError`` +### Error Handling + +- +- ``CloudKitError`` +- ``TokenManagerError`` +- ``TokenStorageError`` +- ``CredentialsValidationError`` +- ``InvalidCredentialReason`` +- ``AuthenticationFailedReason`` +- ``NetworkErrorReason`` +- ``InternalErrorReason`` +- ``CredentialAvailability`` + ### Record management - ``CloudKitRecord`` diff --git a/Sources/MistKit/Documentation.docc/HandlingErrors.md b/Sources/MistKit/Documentation.docc/HandlingErrors.md new file mode 100644 index 00000000..9b6ddc22 --- /dev/null +++ b/Sources/MistKit/Documentation.docc/HandlingErrors.md @@ -0,0 +1,183 @@ +# Handling Errors + +Three layers of typed errors — construction, authentication, and request — surface MistKit failures with enough detail to route, retry, or report. + +## Overview + +Every MistKit failure is one of a small set of typed errors thrown at a specific layer: + +| Layer | Error type | Surfaces from | +| --- | --- | --- | +| Construction | ``CredentialsValidationError`` | ``Credentials`` initializer | +| Token management | ``TokenManagerError`` | Token manager resolution and refresh | +| Token storage | ``TokenStorageError`` | Custom ``TokenStorage`` implementations | +| Request | ``CloudKitError`` | Every ``CloudKitService`` operation | + +Operation methods declare typed throws — `async throws(CloudKitError)` — so the compiler enforces exhaustive switching at the call site if you choose to switch. + +## Construction-time validation + +``Credentials`` runs cheap validation on the values you pass in. The only thrown case is ``CredentialsValidationError/empty``, raised when neither ``Credentials/serverToServer`` nor ``Credentials/apiAuth`` is populated: + +```swift +do { + let credentials = try Credentials() // both nil +} catch CredentialsValidationError.empty { + // Misconfigured deployment — log and exit, this is a programming error. +} +``` + +Deeper per-field reasons (empty token, malformed PEM, key-ID format) live in ``InvalidCredentialReason`` and surface later, wrapped in ``TokenManagerError/invalidCredentials(_:)``, when the credentials are actually used to authenticate. + +> Note: In debug builds, ``Credentials/init(serverToServer:apiAuth:)`` asserts on empty input. In release it throws. Treat this as a configuration check at process start, not a runtime branch. + +## Token manager errors + +``TokenManagerError`` is thrown from authentication resolution — when MistKit picks a token manager for the current call's ``Database``, validates its inputs, or signs the outgoing request. + +| Case | Recoverable? | Typical cause | +| --- | --- | --- | +| ``TokenManagerError/invalidCredentials(_:)`` | No | Malformed token or key surfaces at first use | +| ``TokenManagerError/authenticationFailed(_:)`` | Sometimes | Server-side rejection or signing error | +| ``TokenManagerError/tokenExpired`` | Yes | Refresh the web-auth token and retry | +| ``TokenManagerError/networkError(_:)`` | Yes | Transient connectivity during auth | +| ``TokenManagerError/internalError(_:)`` | No | Bug or missing platform support | + +The wrapped reason enums (``InvalidCredentialReason``, ``AuthenticationFailedReason``, ``NetworkErrorReason``, ``InternalErrorReason``) carry the concrete cause and a human-readable `description`. Switch on the wrapper first, then on the reason if you need to act on it: + +```swift +do { + _ = try await service.fetchCaller() +} catch let error as TokenManagerError { + switch error { + case .tokenExpired: + try await refreshWebAuthToken() + case .invalidCredentials(let reason): + logger.error("Bad credentials: \(reason.description)") + case .networkError, .authenticationFailed, .internalError: + throw error + } +} +``` + +> Tip: ``TokenManagerError/invalidCredentials(_:)`` with reason ``InvalidCredentialReason/serverToServerOnlySupportsPublicDatabase(_:)`` indicates a programming error — you tried `.private` or `.shared` with server-to-server credentials. Fix the call site; do not retry. + +## Token storage errors + +``TokenStorageError`` is for custom ``TokenStorage`` implementations — keychain wrappers, encrypted file stores, remote secret managers. The built-in `InMemoryTokenStorage` never throws it. If you implement ``TokenStorage`` yourself, raise these so MistKit's diagnostics stay consistent: + +- ``TokenStorageError/storageFailed(reason:)`` — write failed, include backend detail +- ``TokenStorageError/notFound(identifier:)`` — credential lookup missed +- ``TokenStorageError/accessDenied`` — OS-level permission failure (keychain locked, etc.) +- ``TokenStorageError/corruptedStorage`` — stored bytes failed to deserialize + +## Request errors + +Every operation on ``CloudKitService`` throws ``CloudKitError``. The cases group naturally by recoverability: + +| Case | Recoverable? | When it fires | +| --- | --- | --- | +| ``CloudKitError/httpError(statusCode:)`` | Depends on status code | HTTP non-2xx without parseable body | +| ``CloudKitError/httpErrorWithDetails(statusCode:serverErrorCode:reason:)`` | Depends on `serverErrorCode` | CloudKit returned a structured error | +| ``CloudKitError/httpErrorWithRawResponse(statusCode:rawResponse:)`` | Sometimes | Validation rejection or unparseable error body | +| ``CloudKitError/invalidResponse`` | No | Server returned 2xx but no payload | +| ``CloudKitError/networkError(_:)`` | Often | URLSession-level failure (timeout, DNS, TLS) | +| ``CloudKitError/decodingError(_:)`` | No | Schema mismatch between OpenAPI client and CloudKit | +| ``CloudKitError/underlyingError(_:)`` | Depends | Unclassified throw from the transport stack | +| ``CloudKitError/unsupportedOperationType(_:)`` | No | Bug — server returned a record op MistKit doesn't model | +| ``CloudKitError/paginationLimitExceeded(maxPages:records:)`` | Yes — but inspect | Auto-paginator hit its guard with records still collected | +| ``CloudKitError/missingCredentials(database:availability:reason:)`` | No | The configured ``Credentials`` don't cover the call's database/auth combo | +| ``CloudKitError/invalidPrivateKey(path:underlying:)`` | No | S2S key file is missing, unreadable, or not a valid ECDSA P-256 PEM | + +Use ``CloudKitError/httpStatusCode`` to read the status code uniformly across the three `httpError*` cases without switching: + +```swift +do { + let records = try await service.queryRecords( + recordType: "Note", + database: .private + ).records +} catch let error as CloudKitError { + if let status = error.httpStatusCode, (500..<600).contains(status) { + // Transient — caller can retry with backoff. + throw RetryableError.serverError(status) + } + throw error +} +``` + +### `paginationLimitExceeded` carries partial results + +``CloudKitService/queryAllRecords(recordType:filters:sortBy:pageSize:desiredKeys:maxPages:database:)`` walks the continuation marker for you and stops at `maxPages` (default `1_000`) as a runaway guard. When it trips, the records collected so far are attached to the error so the caller can decide: + +```swift +do { + let all = try await service.queryAllRecords( + recordType: "AuditEvent", + pageSize: 200, + database: .private + ) +} catch CloudKitError.paginationLimitExceeded(let maxPages, let partial) { + logger.warning("Stopped after \(maxPages) pages with \(partial.count) records") + // Either accept the partial result, raise the cap, or narrow the query. +} +``` + +## Retry and recovery + +Recoverable failures fall into three buckets: + +1. **Transient network** (``CloudKitError/networkError(_:)``, ``TokenManagerError/networkError(_:)``) — retry with exponential backoff and a jitter, bounded to ~3 attempts. +2. **Expired web-auth token** (``TokenManagerError/tokenExpired``) — refresh the token in your token store, then retry the call once. +3. **5xx HTTP** (``CloudKitError/httpStatusCode`` in `500..<600`) — same backoff strategy as transient network. + +Everything else — auth failures, decoding errors, missing credentials, 4xx HTTP — is a programming or configuration error. Surface it, do not retry. + +A minimal retry helper: + +```swift +func retrying( + attempts: Int = 3, + _ operation: () async throws -> T +) async throws -> T { + var lastError: any Error + for attempt in 1...attempts { + do { + return try await operation() + } catch let error as CloudKitError where isTransient(error) { + lastError = error + try await Task.sleep(for: .milliseconds(100 * (1 << attempt))) + } catch { + throw error + } + } + throw lastError +} + +func isTransient(_ error: CloudKitError) -> Bool { + if case .networkError = error { return true } + if let status = error.httpStatusCode, (500..<600).contains(status) { + return true + } + return false +} +``` + +> Warning: Do not retry ``CloudKitError/missingCredentials(database:availability:reason:)`` or ``CloudKitError/invalidPrivateKey(path:underlying:)``. These indicate the deployment is misconfigured and the next attempt will fail identically. + +## Topics + +### Error types + +- ``CloudKitError`` +- ``TokenManagerError`` +- ``TokenStorageError`` +- ``CredentialsValidationError`` + +### Failure reasons + +- ``InvalidCredentialReason`` +- ``AuthenticationFailedReason`` +- ``NetworkErrorReason`` +- ``InternalErrorReason`` +- ``CredentialAvailability`` diff --git a/Sources/MistKit/Documentation.docc/WorkingWithRecords.md b/Sources/MistKit/Documentation.docc/WorkingWithRecords.md new file mode 100644 index 00000000..18ead4d6 --- /dev/null +++ b/Sources/MistKit/Documentation.docc/WorkingWithRecords.md @@ -0,0 +1,192 @@ +# Working with Records + +CRUD, batch, and lookup against CloudKit records — the operations you'll reach for most often, with idiomatic ``CloudKitService`` snippets. + +## Overview + +``CloudKitService`` exposes the CloudKit record lifecycle as a handful of focused async methods. This article walks the full surface so you can pick the right one without reading every operation file. For per-method examples, see the inline DocC on each method; for sync-via-change-tokens, see the linked sync section at the end; for limits and performance, see . + +## Querying + +Use ``CloudKitService/queryRecords(recordType:filters:sortBy:limit:desiredKeys:continuationMarker:database:)`` for a single page of results. Filters are built with ``QueryFilter`` factories, sorts with ``QuerySort/ascending(_:)`` / ``QuerySort/descending(_:)``: + +```swift +let result = try await service.queryRecords( + recordType: "Article", + filters: [ + .greaterThan("publishedDate", .date(oneWeekAgo)), + .equals("status", .string("published")) + ], + sortBy: [.descending("publishedDate")], + limit: 50, + database: .private +) +for record in result.records { + print(record.recordName) +} +``` + +For unbounded iteration, ``CloudKitService/queryAllRecords(recordType:filters:sortBy:pageSize:desiredKeys:maxPages:database:)`` walks the continuation marker for you with a safety guard at `maxPages` (default `1_000`): + +```swift +let allArticles = try await service.queryAllRecords( + recordType: "Article", + pageSize: 200, + database: .private +) +``` + +> Warning: If `queryAllRecords` hits its page cap, it throws ``CloudKitError/paginationLimitExceeded(maxPages:records:)`` with the records collected so far. See for the recovery pattern. + +## Creating + +Use ``CloudKitService/createRecord(recordType:recordName:fields:database:)`` for a single create. Fields are a `[String: FieldValue]` dictionary — every CloudKit scalar plus references, locations, assets, and lists are modeled in ``FieldValue``: + +```swift +let article = try await service.createRecord( + recordType: "Article", + fields: [ + "title": .string("Hello, CloudKit"), + "body": .string("First post."), + "wordCount": .int64(2), + "publishedDate": .date(Date()) + ], + database: .private +) +``` + +Omit `recordName` to let CloudKit generate one; pass an explicit string when you need a stable identifier you can lookup later. + +## Updating + +Use ``CloudKitService/updateRecord(recordType:recordName:fields:recordChangeTag:database:)``. Pass `recordChangeTag` to opt into optimistic concurrency — CloudKit rejects the write if the record has been modified since you read it: + +```swift +let updated = try await service.updateRecord( + recordType: "Article", + recordName: existing.recordName, + fields: ["title": .string("Hello, CloudKit (revised)")], + recordChangeTag: existing.recordChangeTag, + database: .private +) +``` + +> Tip: Omitting `recordChangeTag` lets the write win unconditionally (last-writer-wins). Pass the tag whenever the user's intent depends on seeing the current state — collaborative editing, counters, anything where a stale read produces wrong results. + +## Deleting + +Use ``CloudKitService/deleteRecord(recordType:recordName:recordChangeTag:database:)``: + +```swift +try await service.deleteRecord( + recordType: "Article", + recordName: stale.recordName, + database: .private +) +``` + +Pass `recordChangeTag` to refuse the delete if the record changed since you read it. + +## Batching + +When you need to create, update, and delete in one round-trip, use ``CloudKitService/modifyRecords(_:atomic:database:)`` with an array of ``RecordOperation`` values. The convenience factories ``RecordOperation/create(recordType:recordName:fields:)``, ``RecordOperation/update(recordType:recordName:fields:recordChangeTag:)``, and ``RecordOperation/delete(recordType:recordName:recordChangeTag:)`` keep call sites readable: + +```swift +let results = try await service.modifyRecords( + [ + .create( + recordType: "Article", + fields: ["title": .string("New")] + ), + .update( + recordType: "Article", + recordName: "existing-id", + fields: ["title": .string("Renamed")], + recordChangeTag: existing.recordChangeTag + ), + .delete( + recordType: "Article", + recordName: "old-id" + ) + ], + atomic: true, + database: .private +) +``` + +| `atomic:` | Behavior | +| --- | --- | +| `false` (default) | Per-operation success/failure. Successful ops commit; failed ops surface in the response. | +| `true` | All-or-nothing. If any op fails, none commit. | + +Choose `atomic: true` when the operations are semantically linked (paired updates, a transactional rename) and `false` when independent operations are batched purely for throughput. + +> Note: CloudKit caps batch size around 200 operations per request. See for batching guidance. + +## Looking up + +Use ``CloudKitService/lookupRecords(recordNames:desiredKeys:database:)`` to fetch known records by name: + +```swift +let articles = try await service.lookupRecords( + recordNames: ["article-001", "article-002", "article-003"], + desiredKeys: ["title", "publishedDate"], + database: .private +) +``` + +Pass `desiredKeys` to limit which fields come back — useful for list views that only need a subset. + +## Syncing via change tokens + +For incremental sync — pulling only what changed since the last fetch — use ``CloudKitService/fetchRecordChanges(zoneID:syncToken:resultsLimit:database:)`` (single page) or ``CloudKitService/fetchAllRecordChanges(recordType:syncToken:)`` (auto-paginated). The returned ``RecordChangesResult`` carries a fresh `syncToken` to persist for the next call: + +```swift +var token: String? = loadStoredToken() +repeat { + let result = try await service.fetchRecordChanges( + syncToken: token, + database: .private + ) + process(result.records) + token = result.syncToken +} while result.moreComing +saveToken(token) +``` + +The inline DocC on these methods carries fuller examples for initial-vs-incremental sync. + +## Topics + +### Read operations + +- ``CloudKitService/queryRecords(recordType:filters:sortBy:limit:desiredKeys:continuationMarker:database:)`` +- ``CloudKitService/queryAllRecords(recordType:filters:sortBy:pageSize:desiredKeys:maxPages:database:)`` +- ``CloudKitService/lookupRecords(recordNames:desiredKeys:database:)`` + +### Write operations + +- ``CloudKitService/createRecord(recordType:recordName:fields:database:)`` +- ``CloudKitService/updateRecord(recordType:recordName:fields:recordChangeTag:database:)`` +- ``CloudKitService/deleteRecord(recordType:recordName:recordChangeTag:database:)`` +- ``CloudKitService/modifyRecords(_:atomic:database:)`` + +### Sync + +- ``CloudKitService/fetchRecordChanges(zoneID:syncToken:resultsLimit:database:)`` +- ``CloudKitService/fetchAllRecordChanges(recordType:syncToken:)`` +- ``RecordChangesResult`` + +### Building filters and sorts + +- ``QueryFilter`` +- ``QuerySort`` +- ``FieldValue`` +- ``RecordOperation`` +- ``RecordInfo`` + +### See Also + +- +- +- From abff797b5cea271cf680f60e3e6d34ea359925d9 Mon Sep 17 00:00:00 2001 From: leogdion Date: Tue, 19 May 2026 09:02:29 +0100 Subject: [PATCH 04/35] Style & error audit: explicit import access + scoped flake gates (#159, #334) (#363) --- .github/workflows/MistDemo.yml | 8 +++ CLAUDE.md | 10 +++ .../BushelCloudCLI/BushelCloudCLI.swift | 2 +- .../Commands/ClearCommand.swift | 8 +-- .../Commands/ExportCommand.swift | 8 +-- .../BushelCloudCLI/Commands/ListCommand.swift | 8 +-- .../Commands/StatusCommand.swift | 6 +- .../BushelCloudCLI/Commands/SyncCommand.swift | 8 +-- .../CloudKit/BushelCloudKitService.swift | 4 +- .../CloudKit/CloudKitAuthMethod.swift | 2 +- .../CloudKit/KeyIDValidator.swift | 2 +- .../CloudKit/OperationClassification.swift | 2 +- .../CloudKit/PEMValidator.swift | 2 +- .../CloudKit/SyncEngine+Export.swift | 8 +-- .../BushelCloudKit/CloudKit/SyncEngine.swift | 4 +- .../Configuration/BushelConfiguration.swift | 4 +- .../Configuration/CloudKitConfiguration.swift | 2 +- .../Configuration/CommandConfigurations.swift | 2 +- .../Configuration/ConfigurationKeys.swift | 4 +- .../ConfigurationLoader+Loading.swift | 4 +- .../Configuration/ConfigurationLoader.swift | 6 +- .../DataSources/AppleDB/AppleDBEntry.swift | 2 +- .../DataSources/AppleDB/AppleDBFetcher.swift | 10 +-- .../DataSources/AppleDB/AppleDBHashes.swift | 2 +- .../DataSources/AppleDB/AppleDBLink.swift | 2 +- .../DataSources/AppleDB/AppleDBSource.swift | 2 +- .../DataSources/AppleDB/GitHubCommit.swift | 2 +- .../AppleDB/GitHubCommitsResponse.swift | 2 +- .../DataSources/AppleDB/GitHubCommitter.swift | 2 +- .../DataSources/AppleDB/SignedStatus.swift | 2 +- .../DataSourcePipeline+Deduplication.swift | 4 +- .../DataSourcePipeline+Fetchers.swift | 4 +- ...taSourcePipeline+ReferenceResolution.swift | 2 +- .../DataSources/DataSourcePipeline.swift | 4 +- .../DataSources/IPSWFetcher.swift | 14 ++--- .../DataSources/MESUFetcher.swift | 6 +- .../DataSources/MrMacintoshFetcher.swift | 12 ++-- .../DataSources/SwiftVersionFetcher.swift | 8 +-- .../DataSources/TheAppleWiki/IPSWParser.swift | 4 +- .../TheAppleWiki/Models/IPSWVersion.swift | 2 +- .../TheAppleWiki/Models/ParseContent.swift | 2 +- .../TheAppleWiki/Models/ParseResponse.swift | 2 +- .../TheAppleWiki/Models/TextContent.swift | 2 +- .../TheAppleWiki/TheAppleWikiFetcher.swift | 8 +-- .../DataSources/VirtualBuddyFetcher.swift | 10 +-- .../DataSources/XcodeReleasesFetcher.swift | 8 +-- .../Utilities/ConsoleOutput.swift | 4 +- .../Sources/ConfigKeyKit/ConfigKey.swift | 2 +- .../ConfigKeyKit/ConfigurationKey.swift | 2 +- .../ConfigKeyKit/OptionalConfigKey.swift | 2 +- .../CloudKit/MockCloudKitServiceTests.swift | 8 +-- .../CloudKit/PEMValidatorTests.swift | 2 +- .../ConfigurationLoaderTests.swift | 8 +-- .../FetchConfigurationTests.swift | 4 +- .../DataSources/MockAppleDBFetcherTests.swift | 4 +- .../DataSources/MockIPSWFetcherTests.swift | 4 +- .../DataSources/MockMESUFetcherTests.swift | 4 +- .../MockSwiftVersionFetcherTests.swift | 4 +- .../MockXcodeReleasesFetcherTests.swift | 4 +- .../RestoreImageDeduplicationTests.swift | 4 +- .../DataSources/RestoreImageMergeTests.swift | 4 +- .../SwiftVersionDeduplicationTests.swift | 4 +- .../VirtualBuddyFetcherTests.swift | 6 +- .../XcodeVersionDeduplicationTests.swift | 4 +- ...XcodeVersionReferenceResolutionTests.swift | 4 +- .../AuthenticationErrorHandlingTests.swift | 4 +- .../CloudKitErrorHandlingTests.swift | 6 +- .../GracefulDegradationTests.swift | 4 +- .../NetworkErrorHandlingTests.swift | 4 +- .../Extensions/FieldValueURLTests.swift | 6 +- .../Mocks/MockAppleDBFetcher.swift | 2 +- .../Mocks/MockCloudKitService.swift | 4 +- .../Mocks/MockFetcherError.swift | 2 +- .../Mocks/MockIPSWFetcher.swift | 2 +- .../Mocks/MockMESUFetcher.swift | 2 +- .../Mocks/MockSwiftVersionFetcher.swift | 2 +- .../Mocks/MockURLProtocol.swift | 2 +- .../Mocks/MockXcodeReleasesFetcher.swift | 2 +- .../Models/DataSourceMetadataTests.swift | 6 +- .../Models/RestoreImageRecordTests.swift | 6 +- .../Models/SwiftVersionRecordTests.swift | 4 +- .../Models/XcodeVersionRecordTests.swift | 4 +- .../Utilities/FieldValue+Assertions.swift | 2 +- .../ConfigKeySourceTests.swift | 2 +- .../ConfigKeyKitTests/ConfigKeyTests.swift | 2 +- .../ConfigKeyKitTests/NamingStyleTests.swift | 2 +- .../OptionalConfigKeyTests.swift | 2 +- .../Sources/CelestraCloud/Celestra.swift | 2 +- .../Commands/AddFeedCommand.swift | 8 +-- .../CelestraCloud/Commands/ClearCommand.swift | 8 +-- .../Commands/UpdateCommand+Reporting.swift | 8 +-- .../Commands/UpdateCommand.swift | 8 +-- .../Commands/UpdateCommandError.swift | 2 +- .../Commands/UpdateSummary.swift | 2 +- .../Services/FeedUpdateProcessor+Fetch.swift | 8 +-- .../Services/FeedUpdateProcessor.swift | 8 +-- .../Services/ArticleCloudKitService.swift | 2 +- .../Services/FeedCloudKitService.swift | 2 +- .../CloudKitConfigurationTests.swift | 6 +- .../UpdateCommandConfigurationTests.swift | 4 +- .../CelestraErrorTests+Description.swift | 6 +- ...elestraErrorTests+RecoverySuggestion.swift | 6 +- .../Errors/CelestraErrorTests.swift | 6 +- .../Errors/CloudKitConversionErrorTests.swift | 4 +- .../ArticleConversion+FromCloudKit.swift | 8 +-- .../ArticleConversion+ToCloudKit.swift | 8 +-- .../FeedConversion+FromCloudKit.swift | 8 +-- .../Extensions/FeedConversion+RoundTrip.swift | 8 +-- .../FeedConversion+ToCloudKit.swift | 8 +-- .../Mocks/MockCloudKitRecordOperator.swift | 6 +- .../Models/BatchOperationResultTests.swift | 8 +-- .../ArticleCategorizer+Advanced.swift | 6 +- .../Services/ArticleCategorizer+Basic.swift | 6 +- .../ArticleCloudKitService+Mutations.swift | 8 +-- .../ArticleCloudKitService+Query.swift | 8 +-- .../Services/FeedCloudKitService+CRUD.swift | 8 +-- .../Services/FeedCloudKitService+Query.swift | 8 +-- .../Services/FeedMetadataBuilder+Error.swift | 6 +- .../FeedMetadataBuilder+NotModified.swift | 6 +- .../FeedMetadataBuilder+Success.swift | 6 +- Examples/MistDemo/App/MistDemoApp.swift | 4 +- .../Sources/ConfigKeyKit/Command.swift | 2 +- .../ConfigKeyKit/CommandLineParser.swift | 2 +- .../ConfigKeyKit/CommandRegistry.swift | 2 +- .../Sources/ConfigKeyKit/ConfigKey+Bool.swift | 2 +- .../Sources/ConfigKeyKit/ConfigKey.swift | 2 +- .../ConfigKeyKit/ConfigurationKey.swift | 2 +- .../ConfigKeyKit/ConfigurationParseable.swift | 2 +- .../ConfigKeyKit/OptionalConfigKey.swift | 2 +- .../ConfigKeyKit/StandardNamingStyle.swift | 2 +- .../MistDemo/Sources/MistDemo/MistDemo.swift | 2 +- .../Models/CKRecord+TypedField.swift | 4 +- .../Sources/MistDemoApp/Models/Note.swift | 6 +- .../Sources/MistDemoApp/Models/ZoneRow.swift | 6 +- .../Services/CKDatabase+WebAuthToken.swift | 2 +- .../Services/CKDatabase.Scope+Demo.swift | 2 +- .../MistDemoApp/Services/CloudKitStore.swift | 6 +- .../Services/CloudKitStoreError.swift | 4 +- .../Views/AccountView+Actions.swift | 8 +-- .../MistDemoApp/Views/AccountView.swift | 8 +-- .../MistDemoApp/Views/DetailColumnRoot.swift | 2 +- .../MistDemoApp/Views/NoteEditView.swift | 6 +- .../Sources/MistDemoApp/Views/QueryView.swift | 4 +- .../MistDemoApp/Views/RecordDetailView.swift | 4 +- .../MistDemoApp/Views/SidebarView.swift | 2 +- .../MistDemoApp/Views/ZoneListView.swift | 4 +- .../MistDemoKit/Commands/CreateCommand.swift | 4 +- .../Commands/CurrentUserCommand.swift | 4 +- .../MistDemoKit/Commands/DeleteCommand.swift | 4 +- .../MistDemoKit/Commands/DeleteResult.swift | 2 +- .../Commands/DemoErrorsCommand.swift | 4 +- .../Commands/DemoErrorsRunner+Output.swift | 4 +- .../Commands/DemoErrorsRunner.swift | 4 +- .../Commands/DemoInFilterCommand.swift | 4 +- .../Commands/FetchChangesCommand.swift | 4 +- .../MistDemoKit/Commands/LookupCommand.swift | 4 +- .../Commands/LookupZonesCommand.swift | 4 +- .../Commands/MistDemoCommand.swift | 2 +- .../MistDemoKit/Commands/ModifyCommand.swift | 4 +- .../MistDemoKit/Commands/ModifyOutput.swift | 2 +- .../Commands/ModifyResultRow.swift | 2 +- .../Commands/QueryCommand+FilterParsing.swift | 4 +- .../MistDemoKit/Commands/QueryCommand.swift | 4 +- .../Commands/TestPrivateCommand.swift | 4 +- .../Commands/TestPublicCommand.swift | 4 +- .../MistDemoKit/Commands/UpdateCommand.swift | 4 +- .../Commands/UploadAssetCommand.swift | 4 +- .../Configuration/AuthTokenConfig.swift | 2 +- .../Configuration/BrowserFlagResolver.swift | 2 +- .../Configuration/ConfigurationError.swift | 2 +- .../Configuration/CreateConfig.swift | 2 +- .../Configuration/CurrentUserConfig.swift | 2 +- .../Configuration/DeleteConfig.swift | 2 +- .../Configuration/DemoErrorsConfig.swift | 2 +- .../Configuration/DemoErrorsError.swift | 2 +- .../MistDemoKit/Configuration/Field.swift | 2 +- .../MistDemoKit/Configuration/FieldType.swift | 2 +- .../Configuration/LookupConfig.swift | 2 +- .../Configuration/LookupZonesConfig.swift | 2 +- .../MistDemoConfig+Parsing.swift | 2 +- .../Configuration/MistDemoConfig.swift | 4 +- .../Configuration/MistDemoConfiguration.swift | 6 +- .../Configuration/QueryConfig+Parsing.swift | 2 +- .../Configuration/QueryConfig.swift | 2 +- .../Configuration/TestPrivateConfig.swift | 2 +- .../Configuration/UpdateConfig.swift | 2 +- .../Configuration/UploadAssetConfig.swift | 2 +- .../MistDemoKit/Configuration/WebConfig.swift | 2 +- .../MistDemoConstants+Defaults.swift | 2 +- .../MistDemoConstants+Messages.swift | 2 +- .../Constants/MistDemoConstants.swift | 2 +- .../Errors/ErrorOutput+Convenience.swift | 2 +- .../MistDemoKit/Errors/ErrorOutput.swift | 2 +- .../Errors/FieldConversionError.swift | 2 +- .../MistDemoKit/Errors/MistDemoError.swift | 4 +- .../MistDemoKit/Extensions/Array+Field.swift | 2 +- .../Extensions/Command+AnyCommand.swift | 2 +- .../Extensions/ConfigKey+MistDemo.swift | 2 +- .../Extensions/FieldValue+FieldType.swift | 2 +- .../Extensions/String+Padding.swift | 2 +- .../AssetUploadReceipt+PhaseState.swift | 4 +- .../Integration/CleanupPhaseMarker.swift | 2 +- .../Integration/CreatedRecordNames.swift | 2 +- .../Integration/IncrementalSyncInput.swift | 2 +- .../Integration/IntegrationPhase.swift | 4 +- .../Integration/IntegrationTest.swift | 4 +- .../Integration/IntegrationTestData.swift | 2 +- .../Integration/IntegrationTestError.swift | 2 +- .../Integration/IntegrationTestRunner.swift | 4 +- .../MistDemoKit/Integration/NoState.swift | 2 +- .../Integration/PhaseContext.swift | 4 +- .../MistDemoKit/Integration/PhaseState.swift | 4 +- .../Integration/PhaseStateDecodable.swift | 2 +- .../Integration/PhaseStateEncodable.swift | 2 +- .../Integration/PhasedIntegrationTest.swift | 4 +- .../Integration/Phases/CleanupPhase.swift | 4 +- .../Phases/CreateRecordsPhase.swift | 4 +- .../Phases/DiscoverUserIdentitiesPhase.swift | 4 +- .../Integration/Phases/FetchCallerPhase.swift | 4 +- .../Phases/FetchZoneChangesPhase.swift | 4 +- .../Phases/FinalVerificationPhase.swift | 4 +- .../Phases/IncrementalSyncPhase.swift | 4 +- .../Integration/Phases/InitialSyncPhase.swift | 4 +- .../Integration/Phases/ListZonesPhase.swift | 4 +- .../Phases/LookupRecordsPhase.swift | 4 +- .../Phases/LookupUsersByEmailPhase.swift | 4 +- .../Phases/LookupUsersByRecordNamePhase.swift | 4 +- .../Integration/Phases/LookupZonePhase.swift | 4 +- .../Phases/ModifyRecordsPhase.swift | 4 +- .../Phases/QueryRecordsPhase.swift | 4 +- .../Integration/Phases/UploadAssetPhase.swift | 4 +- .../Integration/SyncTokenSlot.swift | 2 +- .../Tests/PrivateDatabaseTest.swift | 4 +- .../Tests/PublicDatabaseTest.swift | 4 +- .../Integration/UserInfo+PhaseState.swift | 4 +- .../Sources/MistDemoKit/MistDemoRunner.swift | 4 +- .../MistDemoKit/Models/AuthRequest.swift | 2 +- .../Output/Escapers/CSVEscaper.swift | 2 +- .../Output/Escapers/JSONEscaper.swift | 2 +- .../Escapers/OutputEscaperFactory.swift | 2 +- .../Output/Escapers/TableEscaper.swift | 2 +- .../Output/Escapers/YAMLEscaper.swift | 2 +- .../Output/Formatters/CSVFormatter.swift | 4 +- .../Formatters/OutputFormatterFactory.swift | 2 +- .../Output/Formatters/TableFormatter.swift | 4 +- .../Output/Formatters/YAMLFormatter.swift | 4 +- .../MistDemoKit/Output/JSONFormatter.swift | 2 +- .../MistDemoKit/Output/OutputFormat.swift | 2 +- .../MistDemoKit/Output/OutputFormatter.swift | 2 +- .../Output/Protocols/OutputEscaper.swift | 2 +- .../OutputFormatting+Implementations.swift | 4 +- .../Protocols/OutputFormatting+Records.swift | 4 +- .../Protocols/OutputFormatting+Users.swift | 4 +- .../Protocols/OutputFormatting.swift | 4 +- .../MistDemoKit/Types/AnyCodable.swift | 2 +- .../MistDemoKit/Types/DynamicKey.swift | 2 +- .../MistDemoKit/Types/FieldInputValue.swift | 2 +- .../MistDemoKit/Types/FieldsInput.swift | 2 +- .../MistDemoKit/Utilities/AsyncHelpers.swift | 2 +- .../Utilities/AuthenticationError.swift | 2 +- .../AuthenticationHelper+SetupHelpers.swift | 4 +- .../Utilities/AuthenticationHelper.swift | 4 +- .../Utilities/AuthenticationResult.swift | 4 +- .../MistDemoKit/Utilities/BrowserOpener.swift | 4 +- .../Utilities/FieldValueFormatter.swift | 4 +- ...stKitClientFactoryTests+APITokenOnly.swift | 6 +- ...KitClientFactoryTests+BadCredentials.swift | 6 +- ...ientFactoryTests+ContainerIdentifier.swift | 6 +- ...lientFactoryTests+CustomTokenManager.swift | 6 +- ...istKitClientFactoryTests+Environment.swift | 6 +- ...MistKitClientFactoryTests+ErrorCases.swift | 6 +- .../MistKitClientFactoryTests+Helpers.swift | 4 +- ...KitClientFactoryTests+PrivateKeyFile.swift | 6 +- ...KitClientFactoryTests+PublicDatabase.swift | 6 +- ...lientFactoryTests+ServerToServerAuth.swift | 6 +- ...stKitClientFactoryTests+WebAuthToken.swift | 6 +- .../MistKitClientFactoryTests.swift | 2 +- .../AuthTokenCommandTests+AsyncChannel.swift | 6 +- ...enCommandTests+CommandInitialization.swift | 4 +- .../AuthTokenCommandTests+Configuration.swift | 4 +- .../AuthTokenCommandTests+Error.swift | 4 +- .../AuthTokenCommandTests+MockServer.swift | 4 +- .../AuthTokenCommandTests+Timeout.swift | 16 ++--- .../AuthTokenCommandTests.swift | 2 +- ...ionTests+AuthTokenCommandIntegration.swift | 6 +- ...rationTests+CreateCommandIntegration.swift | 6 +- ...grationTests+CrossCommandIntegration.swift | 6 +- ...nTests+CurrentUserCommandIntegration.swift | 6 +- ...rationTests+ErrorHandlingIntegration.swift | 4 +- ...grationTests+QueryCommandIntegration.swift | 6 +- ...rationTests+RealWorldUsageSimulation.swift | 6 +- .../CommandIntegrationTests.swift | 2 +- .../CreateCommandTests+CommandProperty.swift | 6 +- .../CreateCommandTests+Configuration.swift | 6 +- .../CreateCommandTests+ErrorHandling.swift | 4 +- .../CreateCommandTests+FieldParsing.swift | 4 +- .../CreateCommandTests+FieldType.swift | 4 +- ...eateCommandTests+FieldTypeConversion.swift | 4 +- .../CreateCommandTests+FieldValidation.swift | 4 +- ...reateCommandTests+GenerateRecordName.swift | 4 +- .../CreateCommandTests+JSONFieldLoading.swift | 4 +- ...ateCommandTests+MultipleFieldParsing.swift | 4 +- ...ateCommandTests+RecordNameGeneration.swift | 6 +- .../CreateCommand/CreateCommandTests.swift | 2 +- ...rentUserCommandTests+CommandProperty.swift | 6 +- ...urrentUserCommandTests+Configuration.swift | 6 +- ...ntUserCommandTests+DatabaseSelection.swift | 6 +- ...urrentUserCommandTests+ErrorHandling.swift | 4 +- ...rrentUserCommandTests+FieldFiltering.swift | 6 +- ...ests+MistKitClientFactoryIntegration.swift | 6 +- ...entUserCommandTests+MockUserResponse.swift | 4 +- ...CurrentUserCommandTests+OutputFormat.swift | 4 +- .../CurrentUserCommandTests.swift | 2 +- .../DeleteCommandMapConflictTests.swift | 4 +- .../Commands/DeleteCommandTests.swift | 2 +- .../DemoErrorsRunnerOutputTests.swift | 4 +- .../Commands/LookupCommandTests.swift | 4 +- .../Commands/ModifyCommandTests.swift | 4 +- .../Commands/ModifyOutputTests.swift | 4 +- .../Commands/ModifyResultRowTests.swift | 4 +- .../QueryCommandTests+CommandProperty.swift | 6 +- .../QueryCommandTests+Configuration.swift | 6 +- ...QueryCommandTests+ContinuationMarker.swift | 6 +- .../QueryCommandTests+FieldSelection.swift | 6 +- .../QueryCommandTests+FilterParsing.swift | 4 +- .../QueryCommandTests+LimitValidation.swift | 4 +- .../QueryCommandTests+MultipleFilters.swift | 6 +- .../QueryCommandTests+ParseFilter.swift | 6 +- .../QueryCommandTests+RecordType.swift | 6 +- .../QueryCommandTests+SortParsing.swift | 4 +- .../QueryCommandTests+ZoneConfiguration.swift | 6 +- .../QueryCommand/QueryCommandTests.swift | 2 +- .../ConfigKeyKit/CommandLineParserTests.swift | 2 +- ...mmandRegistryTests+AvailableCommands.swift | 4 +- ...CommandRegistryTests+CommandCreation.swift | 4 +- ...ndRegistryTests+CommandTypeRetrieval.swift | 4 +- ...ommandRegistryTests+ConcurrentAccess.swift | 4 +- .../CommandRegistryTests+Errors.swift | 4 +- .../CommandRegistryTests+Metadata.swift | 4 +- .../CommandRegistryTests+Overwrite.swift | 4 +- .../CommandRegistryTests+Registration.swift | 4 +- ...ommandRegistryTests+TestCommandTypes.swift | 2 +- .../CommandRegistryTests.swift | 2 +- .../Configuration/AuthTokenConfigTests.swift | 8 +-- ...tionCredentialsTests+ToConfiguration.swift | 6 +- .../AuthenticationCredentialsTests.swift | 6 +- ...reateConfigTests+BasicInitialization.swift | 6 +- ...ateConfigTests+ComplexInitialization.swift | 6 +- .../CreateConfigTests+EdgeCases.swift | 6 +- ...reateConfigTests+FieldInitialization.swift | 6 +- .../CreateConfigTests+OutputFormat.swift | 6 +- .../CreateConfig/CreateConfigTests.swift | 2 +- ...tUserConfigTests+BasicInitialization.swift | 6 +- ...serConfigTests+ComplexInitialization.swift | 6 +- .../CurrentUserConfigTests+EdgeCases.swift | 6 +- .../CurrentUserConfigTests+Fields.swift | 6 +- .../CurrentUserConfigTests+OutputFormat.swift | 6 +- .../CurrentUserConfigTests.swift | 2 +- .../Configuration/DeleteConfigTests.swift | 4 +- .../Configuration/DeleteErrorTests.swift | 2 +- .../Configuration/DeleteResultTests.swift | 4 +- .../Configuration/DemoErrorsConfigTests.swift | 4 +- .../FetchChangesConfigTests.swift | 4 +- .../Field/FieldTests+BasicParsing.swift | 4 +- .../Field/FieldTests+CaseSensitivity.swift | 4 +- .../Field/FieldTests+ColonHandling.swift | 4 +- .../Field/FieldTests+EdgeCases.swift | 4 +- .../Field/FieldTests+ErrorCases.swift | 4 +- .../Field/FieldTests+ParseMultiple.swift | 4 +- .../Field/FieldTests+WhitespaceHandling.swift | 4 +- .../Configuration/Field/FieldTests.swift | 2 +- ...ieldParsingErrorTests+EmptyFieldName.swift | 4 +- ...FieldParsingErrorTests+InvalidFormat.swift | 4 +- ...arsingErrorTests+InvalidValueForType.swift | 4 +- ...ldParsingErrorTests+UnknownFieldType.swift | 4 +- ...rsingErrorTests+UnsupportedFieldType.swift | 4 +- .../FieldParsingErrorTests.swift | 2 +- .../FieldTypeTests+DoubleConversion.swift | 4 +- .../FieldTypeTests+EnumProperties.swift | 4 +- .../FieldTypeTests+Int64Conversion.swift | 4 +- .../FieldTypeTests+StringConversion.swift | 4 +- .../FieldTypeTests+TimestampConversion.swift | 4 +- .../FieldTypeTests+UnsupportedType.swift | 4 +- .../FieldType/FieldTypeTests.swift | 2 +- .../Configuration/LookupConfigTests.swift | 4 +- .../Configuration/LookupErrorTests.swift | 2 +- .../LookupZonesConfigTests.swift | 4 +- .../Configuration/MistDemoConfigTests.swift | 6 +- .../ModifyConfigParsingTests.swift | 4 +- .../Configuration/ModifyConfigTests.swift | 2 +- .../Configuration/ModifyErrorTests.swift | 2 +- .../ModifyOperationInputTests.swift | 4 +- ...QueryConfigTests+BasicInitialization.swift | 6 +- ...eryConfigTests+ComplexInitialization.swift | 6 +- .../QueryConfigTests+ContinuationMarker.swift | 6 +- .../QueryConfigTests+EdgeCases.swift | 6 +- .../QueryConfigTests+FieldsFilter.swift | 6 +- .../QueryConfig/QueryConfigTests+Filter.swift | 6 +- .../QueryConfig/QueryConfigTests+Limit.swift | 6 +- .../QueryConfig/QueryConfigTests+Offset.swift | 6 +- .../QueryConfigTests+OutputFormat.swift | 6 +- .../QueryConfigTests+SortOption.swift | 6 +- .../QueryConfig/QueryConfigTests.swift | 2 +- .../TestPrivateConfigTests.swift | 6 +- .../Configuration/TestPublicConfigTests.swift | 4 +- ...pdateConfigTests+BasicInitialization.swift | 6 +- .../UpdateConfigTests+CombinedEdgeCases.swift | 6 +- ...pdateConfigTests+FieldInitialization.swift | 6 +- .../UpdateConfigTests+ForceFlag.swift | 6 +- .../UpdateConfigTests+OutputFormat.swift | 6 +- .../UpdateConfig/UpdateConfigTests.swift | 2 +- .../UpdateConfig/UpdateErrorTests.swift | 2 +- .../UploadAssetConfigTests.swift | 4 +- .../CreateErrorTests+ErrorDescription.swift | 4 +- ...CreateErrorTests+ErrorMessageContent.swift | 4 +- .../CreateErrorTests+ErrorThrowing.swift | 4 +- ...ErrorTests+LocalizedErrorConformance.swift | 4 +- .../Errors/CreateError/CreateErrorTests.swift | 2 +- .../Errors/CurrentUserErrorTests.swift | 4 +- .../Errors/ErrorOutputTests.swift | 4 +- .../MistDemoErrorTests+ErrorCode.swift | 6 +- .../MistDemoErrorTests+ErrorDescription.swift | 6 +- .../MistDemoErrorTests+ErrorDetails.swift | 6 +- ...DemoErrorTests+ErrorOutputConversion.swift | 4 +- ...istDemoErrorTests+RecoverySuggestion.swift | 4 +- .../MistDemoError/MistDemoErrorTests.swift | 2 +- .../Errors/QueryErrorTests.swift | 4 +- ...DemoTests+BooleanConfigKeyWithPrefix.swift | 6 +- ...ey+MistDemoTests+ConfigKeyWithPrefix.swift | 6 +- .../ConfigKey+MistDemoTests+EdgeCases.swift | 6 +- ...emoTests+OptionalConfigKeyWithPrefix.swift | 6 +- ...nfigKey+MistDemoTests+RealWorldUsage.swift | 6 +- .../ConfigKey+MistDemoTests.swift | 2 +- .../FieldValue+FieldTypeTests+BytesType.swift | 6 +- ...FieldValue+FieldTypeTests+DoubleType.swift | 6 +- .../FieldValue+FieldTypeTests+Int64Type.swift | 6 +- ...FieldTypeTests+InvalidTypeConversion.swift | 6 +- ...FieldValue+FieldTypeTests+StringType.swift | 6 +- ...lue+FieldTypeTests+TimestampDateType.swift | 6 +- ...Value+FieldTypeTests+UnsupportedType.swift | 6 +- .../FieldValue+FieldTypeTests.swift | 2 +- .../MistDemoConfig+Testing.swift | 6 +- .../CSVEscaperTests+Combination.swift | 4 +- .../CSVEscaperTests+CommaEscaping.swift | 4 +- .../CSVEscaperTests+EdgeCases.swift | 4 +- .../CSVEscaperTests+NewlineEscaping.swift | 4 +- .../CSVEscaperTests+PlainString.swift | 4 +- .../CSVEscaperTests+QuoteEscaping.swift | 4 +- .../CSVEscaperTests+TabEscaping.swift | 4 +- .../CSVEscaperTests+UnicodeAndEmoji.swift | 4 +- .../Escapers/CSVEscaper/CSVEscaperTests.swift | 2 +- .../JSONEscaperTests+BackslashEscaping.swift | 4 +- .../JSONEscaperTests+BackspaceEscaping.swift | 4 +- ...NEscaperTests+CarriageReturnEscaping.swift | 4 +- .../JSONEscaperTests+Combination.swift | 4 +- .../JSONEscaperTests+EdgeCases.swift | 4 +- .../JSONEscaperTests+FormFeedEscaping.swift | 4 +- .../JSONEscaperTests+NewlineEscaping.swift | 4 +- .../JSONEscaperTests+PlainString.swift | 4 +- .../JSONEscaperTests+QuoteEscaping.swift | 4 +- .../JSONEscaperTests+TabEscaping.swift | 4 +- .../JSONEscaperTests+UnicodeAndEmoji.swift | 4 +- .../JSONEscaper/JSONEscaperTests.swift | 2 +- .../Escapers/OutputEscaperFactoryTests.swift | 4 +- ...scaperTests+CarriageReturnConversion.swift | 4 +- .../TableEscaperTests+Combination.swift | 4 +- .../TableEscaperTests+EdgeCases.swift | 4 +- .../TableEscaperTests+NewlineConversion.swift | 4 +- .../TableEscaperTests+PlainString.swift | 4 +- .../TableEscaperTests+TabConversion.swift | 4 +- .../TableEscaperTests+UnicodeAndEmoji.swift | 4 +- ...TableEscaperTests+WhitespaceTrimming.swift | 4 +- .../TableEscaper/TableEscaperTests.swift | 2 +- .../YAMLEscaperTests+BooleanLikeString.swift | 4 +- .../YAMLEscaperTests+ComplexEdgeCases.swift | 4 +- .../YAMLEscaperTests+EmptyString.swift | 4 +- .../YAMLEscaperTests+MultiLineString.swift | 4 +- .../YAMLEscaperTests+NullLikeString.swift | 4 +- .../YAMLEscaperTests+NumericString.swift | 4 +- .../YAMLEscaperTests+PlainString.swift | 4 +- ...caperTests+QuoteAndBackslashEscaping.swift | 4 +- .../YAMLEscaperTests+SpecialCharacter.swift | 4 +- .../YAMLEscaperTests+UnicodeAndEmoji.swift | 4 +- .../YAMLEscaperTests+Whitespace.swift | 4 +- .../YAMLEscaper/YAMLEscaperTests.swift | 2 +- .../CSVFormatterTests+CSVEscaping.swift | 6 +- .../CSVFormatterTests+EdgeCases.swift | 6 +- .../CSVFormatterTests+RecordInfo.swift | 6 +- .../CSVFormatterTests+UserInfo.swift | 6 +- .../CSVFormatter/CSVFormatterTests.swift | 2 +- ...utputFormatterFactoryTests+EdgeCases.swift | 6 +- ...ormatterFactoryTests+FactoryCreation.swift | 6 +- ...terFactoryTests+FormatSpecificOutput.swift | 6 +- ...ryTests+FormatterBehaviorConsistency.swift | 6 +- ...putFormatterFactoryTests+Integration.swift | 6 +- ...rmatterFactoryTests+OutputFormatEnum.swift | 6 +- .../OutputFormatterFactoryTests.swift | 2 +- ...eFormatterTests+EdgeCases+FieldTypes.swift | 6 +- ...eFormatterTests+EdgeCases+Whitespace.swift | 6 +- .../TableFormatterTests+EdgeCases.swift | 2 +- .../TableFormatterTests+RecordInfo.swift | 6 +- ...eFormatterTests+SingleLineConversion.swift | 6 +- .../TableFormatterTests+UserInfo.swift | 6 +- .../TableFormatter/TableFormatterTests.swift | 2 +- .../YAMLFormatterTests+EdgeCases.swift | 6 +- .../YAMLFormatterTests+MultilineString.swift | 6 +- .../YAMLFormatterTests+RecordInfo.swift | 6 +- .../YAMLFormatterTests+UserInfo.swift | 6 +- ...erTests+YAMLEscaping+ReservedStrings.swift | 6 +- ...atterTests+YAMLEscaping+SpecialChars.swift | 6 +- .../YAMLFormatterTests+YAMLEscaping.swift | 2 +- .../YAMLFormatter/YAMLFormatterTests.swift | 2 +- .../Output/JSONFormatterTests.swift | 4 +- .../MistDemoTests/Server/MockBackend.swift | 4 +- .../Server/WebAuthTokenStoreTests.swift | 2 +- .../MistDemoTests/Server/WebJSONTests.swift | 4 +- .../Server/WebServerTests+CRUD.swift | 12 ++-- .../Server/WebServerTests+Database.swift | 12 ++-- .../Server/WebServerTests+Index.swift | 12 ++-- .../Server/WebServerTests+QuerySort.swift | 12 ++-- .../MistDemoTests/Server/WebServerTests.swift | 12 ++-- .../AnyCodableTests+BooleanDecoding.swift | 4 +- .../AnyCodableTests+DoubleDecoding.swift | 4 +- .../AnyCodable/AnyCodableTests+Encoding.swift | 4 +- .../AnyCodable/AnyCodableTests+Errors.swift | 4 +- .../AnyCodableTests+IntegerDecoding.swift | 4 +- .../AnyCodableTests+NullDecoding.swift | 4 +- .../AnyCodableTests+RoundTrip.swift | 4 +- .../AnyCodableTests+StringDecoding.swift | 4 +- .../Types/AnyCodable/AnyCodableTests.swift | 4 +- ...DynamicKeyTests+CodingKeyConformance.swift | 4 +- .../DynamicKey/DynamicKeyTests+Equality.swift | 2 +- ...ynamicKeyTests+IntegerInitialization.swift | 4 +- ...DynamicKeyTests+StringInitialization.swift | 4 +- .../Types/DynamicKey/DynamicKeyTests.swift | 2 +- .../FieldInputValueTests+BoolCase.swift | 4 +- .../FieldInputValueTests+DoubleCase.swift | 4 +- .../FieldInputValueTests+EdgeCases.swift | 4 +- .../FieldInputValueTests+IntCase.swift | 4 +- .../FieldInputValueTests+StringCase.swift | 4 +- .../FieldInputValueTests.swift | 2 +- ...ieldsInputTests+BooleanFieldDecoding.swift | 4 +- ...FieldsInputTests+DoubleFieldDecoding.swift | 4 +- .../FieldsInputTests+Encoding.swift | 4 +- .../FieldsInputTests+FieldName.swift | 4 +- ...ieldsInputTests+IntegerFieldDecoding.swift | 4 +- .../FieldsInputTests+MultipleFields.swift | 4 +- .../FieldsInputTests+SpecialValue.swift | 4 +- ...FieldsInputTests+StringFieldDecoding.swift | 4 +- .../Types/FieldsInput/FieldsInputTests.swift | 2 +- .../MistDemoTests/UserInfoTestExtension.swift | 2 +- .../AsyncHelpersTests+AsyncTimeoutError.swift | 4 +- .../AsyncHelpersTests+ConcurrentTimeout.swift | 27 ++++---- .../AsyncHelpersTests+EdgeCases.swift | 4 +- .../AsyncHelpersTests+FormatTimeout.swift | 4 +- .../AsyncHelpersTests+Timeout.swift | 43 +++++++------ .../AsyncHelpers/AsyncHelpersTests.swift | 2 +- ...ionHelperTests+APIOnlyAuthentication.swift | 4 +- ...erTests+AuthenticationMethodPriority.swift | 4 +- ...erTests+ServerToServerAuthentication.swift | 4 +- ...nticationHelperTests+TokenResolution.swift | 4 +- ...icationHelperTests+WebAuthentication.swift | 4 +- .../AuthenticationHelperTests.swift | 2 +- .../Utilities/EnvironmentTraits.swift | 2 +- .../Utilities/LoopbackAuthorityTests.swift | 2 +- .../Utilities/TestPlatform.swift | 14 +++++ .../Utilities/WithKnownIssueWhen.swift | 61 +++++++++++++++++++ .../Authentication/APITokenManager.swift | 2 +- .../Authentication/AdaptiveTokenManager.swift | 2 +- .../AuthenticationMiddleware.swift | 6 +- .../HTTPField.Name+CloudKit.swift | 2 +- .../Authentication/InternalErrorReason.swift | 2 +- .../Authentication/NetworkErrorReason.swift | 2 +- .../MistKit/Authentication/TokenManager.swift | 2 +- .../Authentication/WebAuthTokenManager.swift | 2 +- .../CloudKitService/CloudKitError.swift | 4 +- .../CloudKitResponseProcessor+Changes.swift | 2 +- .../CloudKitResponseProcessor.swift | 2 +- .../CloudKitService+AssetOperations.swift | 8 +-- .../CloudKitService+AssetUpload.swift | 4 +- .../CloudKitService+Classification.swift | 2 +- .../CloudKitService+ErrorHandling.swift | 6 +- .../CloudKitService+LookupOperations.swift | 2 +- .../CloudKitService+ModifyZones.swift | 8 +-- .../CloudKitService+Operations.swift | 8 +-- .../CloudKitService+QueryPagination.swift | 2 +- .../CloudKitService+RecordManaging.swift | 2 +- .../CloudKitService+SyncOperations.swift | 8 +-- .../CloudKitService+UserOperations.swift | 8 +-- .../CloudKitService+WriteOperations.swift | 8 +-- .../CloudKitService+ZoneOperations.swift | 8 +-- Sources/MistKit/Models/BatchSyncResult.swift | 2 +- .../Models/OperationClassification.swift | 2 +- .../FilterBuilder+ListMemberFilters.swift | 2 +- .../FilterBuilder+StringFilters.swift | 2 +- .../Queries/FilterBuilder/FilterBuilder.swift | 2 +- .../MistKit/Models/Queries/QueryFilter.swift | 2 +- .../MistKit/Models/Queries/QuerySort.swift | 2 +- Sources/MistKit/Models/RecordOperation.swift | 2 +- .../MistKit/OpenAPI/LoggingMiddleware.swift | 6 +- .../CloudKitRecordCollection.swift | 2 +- .../RecordManaging+Generic.swift | 2 +- .../RecordManagement/RecordManaging.swift | 2 +- .../APIToken/APITokenAuthenticatorTests.swift | 8 +-- .../APITokenManager+TestHelpers.swift | 4 +- .../APITokenManagerTests+Manager.swift | 4 +- .../APITokenManagerTests+Metadata.swift | 4 +- .../AdaptiveTokenManager+TestHelpers.swift | 4 +- .../IntegrationTests.swift | 4 +- .../ConcurrentTokenRefreshTests+Basic.swift | 8 +-- .../ConcurrentTokenRefreshTests+Error.swift | 8 +-- .../ConcurrentTokenRefreshTests+Helpers.swift | 8 +-- ...currentTokenRefreshTests+Performance.swift | 8 +-- .../ConcurrentTokenRefreshTests.swift | 2 +- ...ialsTokenManagerTests+PrivateKeyLoad.swift | 4 +- ...tialsTokenManagerTests+PrivateShared.swift | 4 +- ...ialsTokenManagerTests+PublicDatabase.swift | 4 +- ...entialsTokenManagerTests+UserContext.swift | 4 +- .../CredentialsTokenManagerTests.swift | 6 +- .../InMemoryTokenStorage+TestHelpers.swift | 4 +- ...nStorageTests+ConcurrentRemovalTests.swift | 4 +- ...oryTokenStorageTests+ConcurrentTests.swift | 4 +- ...oryTokenStorageTests+ExpirationTests.swift | 4 +- ...okenStorageTests+InitializationTests.swift | 6 +- ...MemoryTokenStorageTests+RemovalTests.swift | 4 +- ...ryTokenStorageTests+ReplacementTests.swift | 4 +- ...moryTokenStorageTests+RetrievalTests.swift | 4 +- ...AuthenticationMiddleware+TestHelpers.swift | 8 +-- ...thenticationMiddlewareTests+APIToken.swift | 10 +-- ...cationMiddlewareTests+Initialization.swift | 10 +-- ...cationMiddlewareTests+ServerToServer.swift | 10 +-- ...uthenticationMiddlewareTests+WebAuth.swift | 10 +-- .../AuthenticationMiddlewareTests.swift | 2 +- .../AuthenticationMiddlewareTests+Error.swift | 10 +-- .../MockTokenManagerWithNetworkError.swift | 2 +- .../NetworkError/RecoveryTests.swift | 10 +-- .../NetworkError/SimulationTests.swift | 10 +-- .../NetworkError/StorageTests.swift | 10 +-- ...erverToServerAuthManager+TestHelpers.swift | 4 +- ...rToServerAuthManagerTests+ErrorTests.swift | 6 +- ...AuthManagerTests+InitializationTests.swift | 6 +- ...rverAuthManagerTests+PrivateKeyTests.swift | 6 +- ...rverAuthManagerTests+ValidationTests.swift | 6 +- .../ServerToServerAuthenticatorTests.swift | 10 +-- .../TokenManagerError+TestHelpers.swift | 4 +- .../TokenManager/TokenManagerErrorTests.swift | 4 +- .../TokenManagerProtocolTests.swift | 4 +- .../TokenManager/TokenManagerTests.swift | 4 +- .../WebAuthTokenAuthenticatorTests.swift | 8 +-- .../WebAuthTokenManager+TestHelpers.swift | 4 +- .../WebAuthTokenManagerTests+Basic.swift | 4 +- .../WebAuthTokenManagerTests+EdgeCases.swift | 4 +- ...WebAuthTokenManagerTests+Performance.swift | 4 +- ...nagerTests+ValidationCredentialTests.swift | 4 +- ...thTokenManagerTests+ValidationFormat.swift | 4 +- ...TokenManagerTests+ValidationWorkflow.swift | 4 +- ...thTokenManagerTests+WebAuthEdgeCases.swift | 4 +- .../CloudKitService/CloudKitErrorTests.swift | 4 +- .../CloudKitServiceTests+Helpers.swift | 4 +- ...Tests.DiscoverUserIdentities+Helpers.swift | 6 +- ....DiscoverUserIdentities+InvalidEmail.swift | 4 +- ....DiscoverUserIdentities+SuccessCases.swift | 4 +- ...ts.DiscoverUserIdentities+Validation.swift | 4 +- ...tServiceTests.DiscoverUserIdentities.swift | 4 +- ...dKitServiceTests.FetchCaller+Helpers.swift | 6 +- ...erviceTests.FetchCaller+SuccessCases.swift | 4 +- ...tServiceTests.FetchCaller+Validation.swift | 4 +- .../CloudKitServiceTests.FetchCaller.swift | 4 +- ...ServiceTests.FetchChanges+Concurrent.swift | 4 +- ...viceTests.FetchChanges+ErrorHandling.swift | 4 +- ...KitServiceTests.FetchChanges+Helpers.swift | 6 +- ...rviceTests.FetchChanges+SuccessCases.swift | 4 +- ...ServiceTests.FetchChanges+Validation.swift | 4 +- ...FetchChanges.SuccessCases+Pagination.swift | 4 +- .../CloudKitServiceTests.FetchChanges.swift | 4 +- ...Tests.FetchZoneChanges+ErrorHandling.swift | 4 +- ...erviceTests.FetchZoneChanges+Helpers.swift | 6 +- ...eTests.FetchZoneChanges+SuccessCases.swift | 4 +- ...iceTests.FetchZoneChanges+Validation.swift | 4 +- ...loudKitServiceTests.FetchZoneChanges.swift | 4 +- ...viceTests.LookupUsersByEmail+Helpers.swift | 4 +- ...ests.LookupUsersByEmail+SuccessCases.swift | 4 +- ...eTests.LookupUsersByEmail+Validation.swift | 4 +- ...udKitServiceTests.LookupUsersByEmail.swift | 4 +- ...ests.LookupUsersByRecordName+Helpers.swift | 4 +- ...LookupUsersByRecordName+SuccessCases.swift | 4 +- ...s.LookupUsersByRecordName+Validation.swift | 4 +- ...ServiceTests.LookupUsersByRecordName.swift | 4 +- ...rviceTests.LookupZones+ErrorHandling.swift | 4 +- ...dKitServiceTests.LookupZones+Helpers.swift | 6 +- ...erviceTests.LookupZones+SuccessCases.swift | 4 +- .../CloudKitServiceTests.LookupZones.swift | 4 +- ...dKitServiceTests.ModifyZones+Helpers.swift | 6 +- ...erviceTests.ModifyZones+SuccessCases.swift | 4 +- .../CloudKitServiceTests.ModifyZones.swift | 4 +- ...dKitServiceTests.Query+Configuration.swift | 4 +- ...CloudKitServiceTests.Query+EdgeCases.swift | 4 +- ...rviceTests.Query+ExistingRecordNames.swift | 4 +- ...tServiceTests.Query+FilterConversion.swift | 4 +- .../CloudKitServiceTests.Query+Helpers.swift | 4 +- ...KitServiceTests.Query+SortConversion.swift | 4 +- .../Query/CloudKitServiceTests.Query.swift | 4 +- ...viceTests.QueryPagination+ErrorCases.swift | 4 +- ...ServiceTests.QueryPagination+Helpers.swift | 6 +- ...ceTests.QueryPagination+SuccessCases.swift | 4 +- ...CloudKitServiceTests.QueryPagination.swift | 4 +- ...KitServiceTests.Upload+ErrorHandling.swift | 4 +- .../CloudKitServiceTests.Upload+Helpers.swift | 6 +- ...KitServiceTests.Upload+NetworkErrors.swift | 4 +- ...dKitServiceTests.Upload+SuccessCases.swift | 4 +- ...oudKitServiceTests.Upload+Validation.swift | 4 +- .../Upload/CloudKitServiceTests.Upload.swift | 4 +- .../RecordOperationConversionTests.swift | 4 +- .../RegexPatternsTests+Convenience.swift | 4 +- .../RegexPatternsTests+Validation.swift | 4 +- .../RegexPatterns/RegexPatternsTests.swift | 2 +- Tests/MistKitTests/Helpers/Platform.swift | 4 +- Tests/MistKitTests/Mocks/MockTransport.swift | 6 +- Tests/MistKitTests/Mocks/ResponseConfig.swift | 4 +- .../MistKitTests/Mocks/ResponseProvider.swift | 4 +- .../MockTokenManagerWithConnectionError.swift | 4 +- ...TokenManagerWithIntermittentFailures.swift | 4 +- .../MockTokenManagerWithRateLimiting.swift | 8 +-- .../MockTokenManagerWithRecovery.swift | 4 +- .../MockTokenManagerWithRefresh.swift | 8 +-- .../MockTokenManagerWithRefreshFailure.swift | 4 +- .../MockTokenManagerWithRefreshTimeout.swift | 8 +-- .../MockTokenManagerWithRetry.swift | 4 +- .../MockTokenManagerWithTimeout.swift | 4 +- .../AssetUploadTokenTests.swift | 4 +- .../Models/BatchSyncResultTests.swift | 4 +- Tests/MistKitTests/Models/DatabaseTests.swift | 4 +- .../Models/EnvironmentTests.swift | 4 +- ...FieldValueConversionTests+BasicTypes.swift | 4 +- ...eldValueConversionTests+ComplexTypes.swift | 4 +- .../FieldValueConversionTests+EdgeCases.swift | 4 +- .../FieldValueConversionTests+Lists.swift | 4 +- .../FieldValueConversionTests.swift | 4 +- .../Models/FieldValues/FieldValueTests.swift | 4 +- .../Models/OperationClassificationTests.swift | 4 +- .../FilterBuilderTests+Comparators.swift | 4 +- .../FilterBuilderTests+ComplexValues.swift | 4 +- .../FilterBuilderTests+ListFilters.swift | 4 +- .../FilterBuilderTests+StringFilters.swift | 4 +- .../FilterBuilder/FilterBuilderTests.swift | 2 +- .../Queries/QueryFilterTests+Comparison.swift | 4 +- .../QueryFilterTests+ComplexFields.swift | 4 +- .../Queries/QueryFilterTests+EdgeCases.swift | 4 +- .../Queries/QueryFilterTests+Equality.swift | 4 +- .../Queries/QueryFilterTests+List.swift | 4 +- .../Queries/QueryFilterTests+ListMember.swift | 4 +- .../Queries/QueryFilterTests+String.swift | 4 +- .../Models/Queries/QueryFilterTests.swift | 4 +- .../Models/Queries/QuerySortTests.swift | 4 +- .../MistKitTests/Models/RecordInfoTests.swift | 4 +- .../LoggingMiddlewareTests+Advanced.swift | 8 +-- .../LoggingMiddlewareTests+Basic.swift | 8 +-- .../LoggingMiddlewareTests+StatusTests.swift | 8 +-- .../LoggingMiddlewareTests.swift | 2 +- .../CloudKitRecordTests+Conformance.swift | 4 +- .../CloudKitRecordTests+FieldConversion.swift | 4 +- .../CloudKitRecordTests+Formatting.swift | 4 +- .../CloudKitRecordTests+Parsing.swift | 4 +- .../CloudKitRecordTests+RoundTrip.swift | 4 +- .../CloudKitRecordTests.swift | 2 +- .../FieldValueConvenienceTests.swift | 4 +- .../MockRecordManagingService.swift | 2 +- .../RecordManagingTests+List.swift | 4 +- .../RecordManagingTests+Query.swift | 4 +- .../RecordManagingTests+Sync.swift | 4 +- .../RecordManagingTests.swift | 2 +- .../RecordManagement/TestRecord.swift | 2 +- 772 files changed, 1778 insertions(+), 1671 deletions(-) create mode 100644 Examples/MistDemo/Tests/MistDemoTests/Utilities/WithKnownIssueWhen.swift diff --git a/.github/workflows/MistDemo.yml b/.github/workflows/MistDemo.yml index d7a4ee70..0c628db9 100644 --- a/.github/workflows/MistDemo.yml +++ b/.github/workflows/MistDemo.yml @@ -280,6 +280,14 @@ jobs: - uses: actions/checkout@v6 - name: Build and Test id: build + # Forward CI=true into the simulator's test process. xcodebuild launches + # simulator tests via simctl, which only propagates env vars prefixed + # with SIMCTL_CHILD_ (stripping the prefix on arrival). Without this, + # `TestPlatform.isFlakyTimeoutSimulator` reads ProcessInfo["CI"] as nil + # inside the sim and the cooperative-executor flake gates in #334 stay + # strict on visionOS/watchOS sim CI runs, surfacing as real failures. + env: + SIMCTL_CHILD_CI: "true" uses: brightdigit/swift-build@v1 with: scheme: ${{ env.PACKAGE_NAME }}-Package diff --git a/CLAUDE.md b/CLAUDE.md index 657ffea7..3a898546 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -426,6 +426,16 @@ For detailed schema workflows and integration: - **AI Schema Workflow** (`Examples/CelestraCloud/.claude/AI_SCHEMA_WORKFLOW.md`) - Comprehensive guide for understanding, designing, modifying, and validating CloudKit schemas with text-based tools - **Quick Reference** (`Examples/SCHEMA_QUICK_REFERENCE.md`) - One-page cheat sheet with syntax, patterns, cktool commands, and troubleshooting +## Import Conventions + +Every `import` statement must carry an explicit access modifier — `internal import X` or `public import X`. Bare `import X` is forbidden. Default to `internal`; use `public import` only when the module's types appear in this file's `public` API (e.g. `public import HTTPTypes` where `HTTPRequest` is part of a `public` signature). + +Exceptions: +- `@testable import …` is its own modifier — no `internal`/`public` prefix. +- `Sources/MistKitOpenAPI/` is generated by swift-openapi-generator and currently emits a single bare `import HTTPTypes` in `Client.swift`. The generator doesn't expose an `accessModifierOnImports` setting yet, so that one line is a documented carve-out (SwiftLint already excludes this directory). + +The convention is not lint-enforced (SwiftLint has no rule for import visibility), so it's a reviewer responsibility plus the precedent set by the codebase after #159. + ## Additional Notes - We are using explicit ACLs in the Swift code - type order is based on the default in swiftlint: https://realm.github.io/SwiftLint/type_contents_order.html diff --git a/Examples/BushelCloud/Sources/BushelCloudCLI/BushelCloudCLI.swift b/Examples/BushelCloud/Sources/BushelCloudCLI/BushelCloudCLI.swift index 5d85bf39..c933076d 100644 --- a/Examples/BushelCloud/Sources/BushelCloudCLI/BushelCloudCLI.swift +++ b/Examples/BushelCloud/Sources/BushelCloudCLI/BushelCloudCLI.swift @@ -27,7 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation +internal import Foundation @main internal struct BushelCloudCLI { diff --git a/Examples/BushelCloud/Sources/BushelCloudCLI/Commands/ClearCommand.swift b/Examples/BushelCloud/Sources/BushelCloudCLI/Commands/ClearCommand.swift index 28a2ed29..8141509c 100644 --- a/Examples/BushelCloud/Sources/BushelCloudCLI/Commands/ClearCommand.swift +++ b/Examples/BushelCloud/Sources/BushelCloudCLI/Commands/ClearCommand.swift @@ -27,10 +27,10 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import BushelCloudKit -import BushelFoundation -import BushelUtilities -import Foundation +internal import BushelCloudKit +internal import BushelFoundation +internal import BushelUtilities +internal import Foundation internal enum ClearCommand { internal static func run(_ args: [String]) async throws { diff --git a/Examples/BushelCloud/Sources/BushelCloudCLI/Commands/ExportCommand.swift b/Examples/BushelCloud/Sources/BushelCloudCLI/Commands/ExportCommand.swift index 13315b6b..9d840cec 100644 --- a/Examples/BushelCloud/Sources/BushelCloudCLI/Commands/ExportCommand.swift +++ b/Examples/BushelCloud/Sources/BushelCloudCLI/Commands/ExportCommand.swift @@ -27,10 +27,10 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import BushelCloudKit -import BushelFoundation -import Foundation -import MistKit +internal import BushelCloudKit +internal import BushelFoundation +internal import Foundation +internal import MistKit internal enum ExportCommand { // MARK: - Export Types diff --git a/Examples/BushelCloud/Sources/BushelCloudCLI/Commands/ListCommand.swift b/Examples/BushelCloud/Sources/BushelCloudCLI/Commands/ListCommand.swift index ce4dced2..51b2aad8 100644 --- a/Examples/BushelCloud/Sources/BushelCloudCLI/Commands/ListCommand.swift +++ b/Examples/BushelCloud/Sources/BushelCloudCLI/Commands/ListCommand.swift @@ -27,10 +27,10 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import BushelCloudKit -import BushelFoundation -import Foundation -import MistKit +internal import BushelCloudKit +internal import BushelFoundation +internal import Foundation +internal import MistKit internal enum ListCommand { internal static func run(_ args: [String]) async throws { diff --git a/Examples/BushelCloud/Sources/BushelCloudCLI/Commands/StatusCommand.swift b/Examples/BushelCloud/Sources/BushelCloudCLI/Commands/StatusCommand.swift index 98b02d87..74090ca1 100644 --- a/Examples/BushelCloud/Sources/BushelCloudCLI/Commands/StatusCommand.swift +++ b/Examples/BushelCloud/Sources/BushelCloudCLI/Commands/StatusCommand.swift @@ -27,9 +27,9 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import BushelCloudKit -import BushelFoundation -import Foundation +internal import BushelCloudKit +internal import BushelFoundation +internal import Foundation internal import MistKit internal enum StatusCommand { diff --git a/Examples/BushelCloud/Sources/BushelCloudCLI/Commands/SyncCommand.swift b/Examples/BushelCloud/Sources/BushelCloudCLI/Commands/SyncCommand.swift index 5313a8bc..77665890 100644 --- a/Examples/BushelCloud/Sources/BushelCloudCLI/Commands/SyncCommand.swift +++ b/Examples/BushelCloud/Sources/BushelCloudCLI/Commands/SyncCommand.swift @@ -27,10 +27,10 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import BushelCloudKit -import BushelFoundation -import BushelUtilities -import Foundation +internal import BushelCloudKit +internal import BushelFoundation +internal import BushelUtilities +internal import Foundation internal enum SyncCommand { internal static func run(_ args: [String]) async throws { diff --git a/Examples/BushelCloud/Sources/BushelCloudKit/CloudKit/BushelCloudKitService.swift b/Examples/BushelCloud/Sources/BushelCloudKit/CloudKit/BushelCloudKitService.swift index 9acbe26e..433f87d3 100644 --- a/Examples/BushelCloud/Sources/BushelCloudKit/CloudKit/BushelCloudKitService.swift +++ b/Examples/BushelCloud/Sources/BushelCloudKit/CloudKit/BushelCloudKitService.swift @@ -30,11 +30,11 @@ public import BushelFoundation public import BushelLogging public import Foundation -import Logging +internal import Logging public import MistKit #if canImport(FelinePineSwift) - import FelinePineSwift + internal import FelinePineSwift #endif /// CloudKit service wrapper for Bushel demo operations diff --git a/Examples/BushelCloud/Sources/BushelCloudKit/CloudKit/CloudKitAuthMethod.swift b/Examples/BushelCloud/Sources/BushelCloudKit/CloudKit/CloudKitAuthMethod.swift index c7d00980..6cf6299d 100644 --- a/Examples/BushelCloud/Sources/BushelCloudKit/CloudKit/CloudKitAuthMethod.swift +++ b/Examples/BushelCloud/Sources/BushelCloudKit/CloudKit/CloudKitAuthMethod.swift @@ -27,7 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation +internal import Foundation /// Authentication method for CloudKit Server-to-Server /// diff --git a/Examples/BushelCloud/Sources/BushelCloudKit/CloudKit/KeyIDValidator.swift b/Examples/BushelCloud/Sources/BushelCloudKit/CloudKit/KeyIDValidator.swift index 88a7cfb7..63de7bb8 100644 --- a/Examples/BushelCloud/Sources/BushelCloudKit/CloudKit/KeyIDValidator.swift +++ b/Examples/BushelCloud/Sources/BushelCloudKit/CloudKit/KeyIDValidator.swift @@ -27,7 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation +internal import Foundation /// Validates CloudKit Server-to-Server Key ID format internal enum KeyIDValidator { diff --git a/Examples/BushelCloud/Sources/BushelCloudKit/CloudKit/OperationClassification.swift b/Examples/BushelCloud/Sources/BushelCloudKit/CloudKit/OperationClassification.swift index 5fc95db4..bf51d0dc 100644 --- a/Examples/BushelCloud/Sources/BushelCloudKit/CloudKit/OperationClassification.swift +++ b/Examples/BushelCloud/Sources/BushelCloudKit/CloudKit/OperationClassification.swift @@ -27,7 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation +internal import Foundation /// Classifies CloudKit operations as creates or updates /// diff --git a/Examples/BushelCloud/Sources/BushelCloudKit/CloudKit/PEMValidator.swift b/Examples/BushelCloud/Sources/BushelCloudKit/CloudKit/PEMValidator.swift index e81ea290..6512f581 100644 --- a/Examples/BushelCloud/Sources/BushelCloudKit/CloudKit/PEMValidator.swift +++ b/Examples/BushelCloud/Sources/BushelCloudKit/CloudKit/PEMValidator.swift @@ -27,7 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation +internal import Foundation /// Validates PEM format for CloudKit Server-to-Server private keys internal enum PEMValidator { diff --git a/Examples/BushelCloud/Sources/BushelCloudKit/CloudKit/SyncEngine+Export.swift b/Examples/BushelCloud/Sources/BushelCloudKit/CloudKit/SyncEngine+Export.swift index 552c51b7..6116e483 100644 --- a/Examples/BushelCloud/Sources/BushelCloudKit/CloudKit/SyncEngine+Export.swift +++ b/Examples/BushelCloud/Sources/BushelCloudKit/CloudKit/SyncEngine+Export.swift @@ -27,13 +27,13 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import BushelLogging -import BushelUtilities -import Logging +internal import BushelLogging +internal import BushelUtilities +internal import Logging public import MistKit #if canImport(FelinePineSwift) - import FelinePineSwift + internal import FelinePineSwift #endif // MARK: - Export Operations diff --git a/Examples/BushelCloud/Sources/BushelCloudKit/CloudKit/SyncEngine.swift b/Examples/BushelCloud/Sources/BushelCloudKit/CloudKit/SyncEngine.swift index eb41ab45..feec6451 100644 --- a/Examples/BushelCloud/Sources/BushelCloudKit/CloudKit/SyncEngine.swift +++ b/Examples/BushelCloud/Sources/BushelCloudKit/CloudKit/SyncEngine.swift @@ -31,11 +31,11 @@ public import BushelFoundation public import BushelLogging public import BushelUtilities public import Foundation -import Logging +internal import Logging public import MistKit #if canImport(FelinePineSwift) - import FelinePineSwift + internal import FelinePineSwift #endif /// Orchestrates the complete sync process from data sources to CloudKit diff --git a/Examples/BushelCloud/Sources/BushelCloudKit/Configuration/BushelConfiguration.swift b/Examples/BushelCloud/Sources/BushelCloudKit/Configuration/BushelConfiguration.swift index 8ff93192..6d3978de 100644 --- a/Examples/BushelCloud/Sources/BushelCloudKit/Configuration/BushelConfiguration.swift +++ b/Examples/BushelCloud/Sources/BushelCloudKit/Configuration/BushelConfiguration.swift @@ -28,8 +28,8 @@ // public import BushelFoundation -import Foundation -import MistKit +internal import Foundation +internal import MistKit // MARK: - Configuration Error diff --git a/Examples/BushelCloud/Sources/BushelCloudKit/Configuration/CloudKitConfiguration.swift b/Examples/BushelCloud/Sources/BushelCloudKit/Configuration/CloudKitConfiguration.swift index e948a3b9..077cdb4f 100644 --- a/Examples/BushelCloud/Sources/BushelCloudKit/Configuration/CloudKitConfiguration.swift +++ b/Examples/BushelCloud/Sources/BushelCloudKit/Configuration/CloudKitConfiguration.swift @@ -27,7 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation +internal import Foundation public import MistKit // MARK: - CloudKit Configuration diff --git a/Examples/BushelCloud/Sources/BushelCloudKit/Configuration/CommandConfigurations.swift b/Examples/BushelCloud/Sources/BushelCloudKit/Configuration/CommandConfigurations.swift index 7b0c257d..2f17f396 100644 --- a/Examples/BushelCloud/Sources/BushelCloudKit/Configuration/CommandConfigurations.swift +++ b/Examples/BushelCloud/Sources/BushelCloudKit/Configuration/CommandConfigurations.swift @@ -27,7 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation +internal import Foundation // MARK: - Sync Configuration diff --git a/Examples/BushelCloud/Sources/BushelCloudKit/Configuration/ConfigurationKeys.swift b/Examples/BushelCloud/Sources/BushelCloudKit/Configuration/ConfigurationKeys.swift index 5547878d..ae75d0f7 100644 --- a/Examples/BushelCloud/Sources/BushelCloudKit/Configuration/ConfigurationKeys.swift +++ b/Examples/BushelCloud/Sources/BushelCloudKit/Configuration/ConfigurationKeys.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import ConfigKeyKit -import Foundation +internal import ConfigKeyKit +internal import Foundation /// Configuration keys for reading from providers internal enum ConfigurationKeys { diff --git a/Examples/BushelCloud/Sources/BushelCloudKit/Configuration/ConfigurationLoader+Loading.swift b/Examples/BushelCloud/Sources/BushelCloudKit/Configuration/ConfigurationLoader+Loading.swift index b64874ed..72fc729f 100644 --- a/Examples/BushelCloud/Sources/BushelCloudKit/Configuration/ConfigurationLoader+Loading.swift +++ b/Examples/BushelCloud/Sources/BushelCloudKit/Configuration/ConfigurationLoader+Loading.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import BushelFoundation -import Foundation +internal import BushelFoundation +internal import Foundation // MARK: - Configuration Loading diff --git a/Examples/BushelCloud/Sources/BushelCloudKit/Configuration/ConfigurationLoader.swift b/Examples/BushelCloud/Sources/BushelCloudKit/Configuration/ConfigurationLoader.swift index 566f7951..6379ecd2 100644 --- a/Examples/BushelCloud/Sources/BushelCloudKit/Configuration/ConfigurationLoader.swift +++ b/Examples/BushelCloud/Sources/BushelCloudKit/Configuration/ConfigurationLoader.swift @@ -27,9 +27,9 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import ConfigKeyKit -import Configuration -import Foundation +internal import ConfigKeyKit +internal import Configuration +internal import Foundation /// Actor responsible for loading configuration from CLI arguments and environment variables public actor ConfigurationLoader { diff --git a/Examples/BushelCloud/Sources/BushelCloudKit/DataSources/AppleDB/AppleDBEntry.swift b/Examples/BushelCloud/Sources/BushelCloudKit/DataSources/AppleDB/AppleDBEntry.swift index edddd6cd..7ee83ff8 100644 --- a/Examples/BushelCloud/Sources/BushelCloudKit/DataSources/AppleDB/AppleDBEntry.swift +++ b/Examples/BushelCloud/Sources/BushelCloudKit/DataSources/AppleDB/AppleDBEntry.swift @@ -27,7 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation +internal import Foundation /// Represents a single macOS build entry from AppleDB internal struct AppleDBEntry: Codable { diff --git a/Examples/BushelCloud/Sources/BushelCloudKit/DataSources/AppleDB/AppleDBFetcher.swift b/Examples/BushelCloud/Sources/BushelCloudKit/DataSources/AppleDB/AppleDBFetcher.swift index 6d1d32fa..83ab1ff4 100644 --- a/Examples/BushelCloud/Sources/BushelCloudKit/DataSources/AppleDB/AppleDBFetcher.swift +++ b/Examples/BushelCloud/Sources/BushelCloudKit/DataSources/AppleDB/AppleDBFetcher.swift @@ -29,16 +29,16 @@ public import BushelFoundation public import BushelLogging -import BushelUtilities -import Foundation -import Logging +internal import BushelUtilities +internal import Foundation +internal import Logging #if canImport(FelinePineSwift) - import FelinePineSwift + internal import FelinePineSwift #endif #if canImport(FoundationNetworking) - import FoundationNetworking + internal import FoundationNetworking #endif /// Fetcher for macOS restore images using AppleDB API diff --git a/Examples/BushelCloud/Sources/BushelCloudKit/DataSources/AppleDB/AppleDBHashes.swift b/Examples/BushelCloud/Sources/BushelCloudKit/DataSources/AppleDB/AppleDBHashes.swift index 5721fe36..dfd214c1 100644 --- a/Examples/BushelCloud/Sources/BushelCloudKit/DataSources/AppleDB/AppleDBHashes.swift +++ b/Examples/BushelCloud/Sources/BushelCloudKit/DataSources/AppleDB/AppleDBHashes.swift @@ -27,7 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation +internal import Foundation /// Represents file hashes for verification internal struct AppleDBHashes: Codable { diff --git a/Examples/BushelCloud/Sources/BushelCloudKit/DataSources/AppleDB/AppleDBLink.swift b/Examples/BushelCloud/Sources/BushelCloudKit/DataSources/AppleDB/AppleDBLink.swift index 5f47a6db..048c3794 100644 --- a/Examples/BushelCloud/Sources/BushelCloudKit/DataSources/AppleDB/AppleDBLink.swift +++ b/Examples/BushelCloud/Sources/BushelCloudKit/DataSources/AppleDB/AppleDBLink.swift @@ -27,7 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation +internal import Foundation /// Represents a download link for a source internal struct AppleDBLink: Codable { diff --git a/Examples/BushelCloud/Sources/BushelCloudKit/DataSources/AppleDB/AppleDBSource.swift b/Examples/BushelCloud/Sources/BushelCloudKit/DataSources/AppleDB/AppleDBSource.swift index 399f0bd9..d684c5c0 100644 --- a/Examples/BushelCloud/Sources/BushelCloudKit/DataSources/AppleDB/AppleDBSource.swift +++ b/Examples/BushelCloud/Sources/BushelCloudKit/DataSources/AppleDB/AppleDBSource.swift @@ -27,7 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation +internal import Foundation /// Represents an installation source (IPSW, OTA, or IA) internal struct AppleDBSource: Codable { diff --git a/Examples/BushelCloud/Sources/BushelCloudKit/DataSources/AppleDB/GitHubCommit.swift b/Examples/BushelCloud/Sources/BushelCloudKit/DataSources/AppleDB/GitHubCommit.swift index 38c3fb63..5ec9c36a 100644 --- a/Examples/BushelCloud/Sources/BushelCloudKit/DataSources/AppleDB/GitHubCommit.swift +++ b/Examples/BushelCloud/Sources/BushelCloudKit/DataSources/AppleDB/GitHubCommit.swift @@ -27,7 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation +internal import Foundation /// Represents a commit in GitHub API response internal struct GitHubCommit: Codable { diff --git a/Examples/BushelCloud/Sources/BushelCloudKit/DataSources/AppleDB/GitHubCommitsResponse.swift b/Examples/BushelCloud/Sources/BushelCloudKit/DataSources/AppleDB/GitHubCommitsResponse.swift index 56954e42..1ba3dd2b 100644 --- a/Examples/BushelCloud/Sources/BushelCloudKit/DataSources/AppleDB/GitHubCommitsResponse.swift +++ b/Examples/BushelCloud/Sources/BushelCloudKit/DataSources/AppleDB/GitHubCommitsResponse.swift @@ -27,7 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation +internal import Foundation /// Response from GitHub API for commits internal struct GitHubCommitsResponse: Codable { diff --git a/Examples/BushelCloud/Sources/BushelCloudKit/DataSources/AppleDB/GitHubCommitter.swift b/Examples/BushelCloud/Sources/BushelCloudKit/DataSources/AppleDB/GitHubCommitter.swift index aee3843d..9f63835b 100644 --- a/Examples/BushelCloud/Sources/BushelCloudKit/DataSources/AppleDB/GitHubCommitter.swift +++ b/Examples/BushelCloud/Sources/BushelCloudKit/DataSources/AppleDB/GitHubCommitter.swift @@ -27,7 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation +internal import Foundation /// Represents a committer in GitHub API response internal struct GitHubCommitter: Codable { diff --git a/Examples/BushelCloud/Sources/BushelCloudKit/DataSources/AppleDB/SignedStatus.swift b/Examples/BushelCloud/Sources/BushelCloudKit/DataSources/AppleDB/SignedStatus.swift index aebc8bbd..b4b36608 100644 --- a/Examples/BushelCloud/Sources/BushelCloudKit/DataSources/AppleDB/SignedStatus.swift +++ b/Examples/BushelCloud/Sources/BushelCloudKit/DataSources/AppleDB/SignedStatus.swift @@ -27,7 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation +internal import Foundation /// Represents the signing status for a build /// Can be: array of device IDs, boolean true (all signed), or empty array (none signed) diff --git a/Examples/BushelCloud/Sources/BushelCloudKit/DataSources/DataSourcePipeline+Deduplication.swift b/Examples/BushelCloud/Sources/BushelCloudKit/DataSources/DataSourcePipeline+Deduplication.swift index 70ca5357..74f7a863 100644 --- a/Examples/BushelCloud/Sources/BushelCloudKit/DataSources/DataSourcePipeline+Deduplication.swift +++ b/Examples/BushelCloud/Sources/BushelCloudKit/DataSources/DataSourcePipeline+Deduplication.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import BushelFoundation -import Foundation +internal import BushelFoundation +internal import Foundation // MARK: - Deduplication extension DataSourcePipeline { diff --git a/Examples/BushelCloud/Sources/BushelCloudKit/DataSources/DataSourcePipeline+Fetchers.swift b/Examples/BushelCloud/Sources/BushelCloudKit/DataSources/DataSourcePipeline+Fetchers.swift index 90b3f797..29397dda 100644 --- a/Examples/BushelCloud/Sources/BushelCloudKit/DataSources/DataSourcePipeline+Fetchers.swift +++ b/Examples/BushelCloud/Sources/BushelCloudKit/DataSources/DataSourcePipeline+Fetchers.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import BushelFoundation -import Foundation +internal import BushelFoundation +internal import Foundation // MARK: - Private Fetching Methods extension DataSourcePipeline { diff --git a/Examples/BushelCloud/Sources/BushelCloudKit/DataSources/DataSourcePipeline+ReferenceResolution.swift b/Examples/BushelCloud/Sources/BushelCloudKit/DataSources/DataSourcePipeline+ReferenceResolution.swift index d3d05627..8d391818 100644 --- a/Examples/BushelCloud/Sources/BushelCloudKit/DataSources/DataSourcePipeline+ReferenceResolution.swift +++ b/Examples/BushelCloud/Sources/BushelCloudKit/DataSources/DataSourcePipeline+ReferenceResolution.swift @@ -27,7 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import BushelFoundation +internal import BushelFoundation // MARK: - Reference Resolution extension DataSourcePipeline { diff --git a/Examples/BushelCloud/Sources/BushelCloudKit/DataSources/DataSourcePipeline.swift b/Examples/BushelCloud/Sources/BushelCloudKit/DataSources/DataSourcePipeline.swift index 021cae83..2693a273 100644 --- a/Examples/BushelCloud/Sources/BushelCloudKit/DataSources/DataSourcePipeline.swift +++ b/Examples/BushelCloud/Sources/BushelCloudKit/DataSources/DataSourcePipeline.swift @@ -28,8 +28,8 @@ // public import BushelFoundation -import BushelLogging -import Foundation +internal import BushelLogging +internal import Foundation /// Orchestrates fetching data from all sources with deduplication and relationship resolution public struct DataSourcePipeline: Sendable { diff --git a/Examples/BushelCloud/Sources/BushelCloudKit/DataSources/IPSWFetcher.swift b/Examples/BushelCloud/Sources/BushelCloudKit/DataSources/IPSWFetcher.swift index 85f3d656..79dbc3d6 100644 --- a/Examples/BushelCloud/Sources/BushelCloudKit/DataSources/IPSWFetcher.swift +++ b/Examples/BushelCloud/Sources/BushelCloudKit/DataSources/IPSWFetcher.swift @@ -27,15 +27,15 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import BushelFoundation -import BushelUtilities -import Foundation -import IPSWDownloads -import OpenAPIURLSession -import OSVer +internal import BushelFoundation +internal import BushelUtilities +internal import Foundation +internal import IPSWDownloads +internal import OSVer +internal import OpenAPIURLSession #if canImport(FoundationNetworking) - import FoundationNetworking + internal import FoundationNetworking #endif /// Fetcher for macOS restore images using the IPSWDownloads package diff --git a/Examples/BushelCloud/Sources/BushelCloudKit/DataSources/MESUFetcher.swift b/Examples/BushelCloud/Sources/BushelCloudKit/DataSources/MESUFetcher.swift index 2717d45b..696d577f 100644 --- a/Examples/BushelCloud/Sources/BushelCloudKit/DataSources/MESUFetcher.swift +++ b/Examples/BushelCloud/Sources/BushelCloudKit/DataSources/MESUFetcher.swift @@ -28,11 +28,11 @@ // public import BushelFoundation -import BushelUtilities -import Foundation +internal import BushelUtilities +internal import Foundation #if canImport(FoundationNetworking) - import FoundationNetworking + internal import FoundationNetworking #endif /// Fetcher for Apple MESU (Mobile Equipment Software Update) manifest diff --git a/Examples/BushelCloud/Sources/BushelCloudKit/DataSources/MrMacintoshFetcher.swift b/Examples/BushelCloud/Sources/BushelCloudKit/DataSources/MrMacintoshFetcher.swift index 99a64f22..cbe5dd84 100644 --- a/Examples/BushelCloud/Sources/BushelCloudKit/DataSources/MrMacintoshFetcher.swift +++ b/Examples/BushelCloud/Sources/BushelCloudKit/DataSources/MrMacintoshFetcher.swift @@ -27,18 +27,18 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import BushelFoundation +internal import BushelFoundation public import BushelLogging -import Foundation -import Logging -import SwiftSoup +internal import Foundation +internal import Logging +internal import SwiftSoup #if canImport(FelinePineSwift) - import FelinePineSwift + internal import FelinePineSwift #endif #if canImport(FoundationNetworking) - import FoundationNetworking + internal import FoundationNetworking #endif /// Fetcher for macOS beta/RC restore images from Mr. Macintosh database diff --git a/Examples/BushelCloud/Sources/BushelCloudKit/DataSources/SwiftVersionFetcher.swift b/Examples/BushelCloud/Sources/BushelCloudKit/DataSources/SwiftVersionFetcher.swift index 3400d1eb..106ddd5a 100644 --- a/Examples/BushelCloud/Sources/BushelCloudKit/DataSources/SwiftVersionFetcher.swift +++ b/Examples/BushelCloud/Sources/BushelCloudKit/DataSources/SwiftVersionFetcher.swift @@ -27,12 +27,12 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import BushelFoundation -import Foundation -import SwiftSoup +internal import BushelFoundation +internal import Foundation +internal import SwiftSoup #if canImport(FoundationNetworking) - import FoundationNetworking + internal import FoundationNetworking #endif /// Fetcher for Swift compiler versions from swiftversion.net diff --git a/Examples/BushelCloud/Sources/BushelCloudKit/DataSources/TheAppleWiki/IPSWParser.swift b/Examples/BushelCloud/Sources/BushelCloudKit/DataSources/TheAppleWiki/IPSWParser.swift index 41305593..10bdf73c 100644 --- a/Examples/BushelCloud/Sources/BushelCloudKit/DataSources/TheAppleWiki/IPSWParser.swift +++ b/Examples/BushelCloud/Sources/BushelCloudKit/DataSources/TheAppleWiki/IPSWParser.swift @@ -27,10 +27,10 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation +internal import Foundation #if canImport(FoundationNetworking) - import FoundationNetworking + internal import FoundationNetworking #endif // MARK: - Errors diff --git a/Examples/BushelCloud/Sources/BushelCloudKit/DataSources/TheAppleWiki/Models/IPSWVersion.swift b/Examples/BushelCloud/Sources/BushelCloudKit/DataSources/TheAppleWiki/Models/IPSWVersion.swift index b020a458..e02b17f1 100644 --- a/Examples/BushelCloud/Sources/BushelCloudKit/DataSources/TheAppleWiki/Models/IPSWVersion.swift +++ b/Examples/BushelCloud/Sources/BushelCloudKit/DataSources/TheAppleWiki/Models/IPSWVersion.swift @@ -27,7 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation +internal import Foundation /// IPSW metadata from TheAppleWiki internal struct IPSWVersion: Codable, Sendable { diff --git a/Examples/BushelCloud/Sources/BushelCloudKit/DataSources/TheAppleWiki/Models/ParseContent.swift b/Examples/BushelCloud/Sources/BushelCloudKit/DataSources/TheAppleWiki/Models/ParseContent.swift index 709c4c7a..b035acfb 100644 --- a/Examples/BushelCloud/Sources/BushelCloudKit/DataSources/TheAppleWiki/Models/ParseContent.swift +++ b/Examples/BushelCloud/Sources/BushelCloudKit/DataSources/TheAppleWiki/Models/ParseContent.swift @@ -27,7 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation +internal import Foundation /// Parse content container internal struct ParseContent: Codable, Sendable { diff --git a/Examples/BushelCloud/Sources/BushelCloudKit/DataSources/TheAppleWiki/Models/ParseResponse.swift b/Examples/BushelCloud/Sources/BushelCloudKit/DataSources/TheAppleWiki/Models/ParseResponse.swift index 0ca9cfde..961665d3 100644 --- a/Examples/BushelCloud/Sources/BushelCloudKit/DataSources/TheAppleWiki/Models/ParseResponse.swift +++ b/Examples/BushelCloud/Sources/BushelCloudKit/DataSources/TheAppleWiki/Models/ParseResponse.swift @@ -27,7 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation +internal import Foundation /// Root response from TheAppleWiki parse API internal struct ParseResponse: Codable, Sendable { diff --git a/Examples/BushelCloud/Sources/BushelCloudKit/DataSources/TheAppleWiki/Models/TextContent.swift b/Examples/BushelCloud/Sources/BushelCloudKit/DataSources/TheAppleWiki/Models/TextContent.swift index 73258bd6..6b8084d4 100644 --- a/Examples/BushelCloud/Sources/BushelCloudKit/DataSources/TheAppleWiki/Models/TextContent.swift +++ b/Examples/BushelCloud/Sources/BushelCloudKit/DataSources/TheAppleWiki/Models/TextContent.swift @@ -27,7 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation +internal import Foundation /// Text content with HTML internal struct TextContent: Codable, Sendable { diff --git a/Examples/BushelCloud/Sources/BushelCloudKit/DataSources/TheAppleWiki/TheAppleWikiFetcher.swift b/Examples/BushelCloud/Sources/BushelCloudKit/DataSources/TheAppleWiki/TheAppleWikiFetcher.swift index 2df6e65d..b6346242 100644 --- a/Examples/BushelCloud/Sources/BushelCloudKit/DataSources/TheAppleWiki/TheAppleWikiFetcher.swift +++ b/Examples/BushelCloud/Sources/BushelCloudKit/DataSources/TheAppleWiki/TheAppleWikiFetcher.swift @@ -27,12 +27,12 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import BushelFoundation -import BushelUtilities -import Foundation +internal import BushelFoundation +internal import BushelUtilities +internal import Foundation #if canImport(FoundationNetworking) - import FoundationNetworking + internal import FoundationNetworking #endif /// Fetcher for macOS restore images using TheAppleWiki.com diff --git a/Examples/BushelCloud/Sources/BushelCloudKit/DataSources/VirtualBuddyFetcher.swift b/Examples/BushelCloud/Sources/BushelCloudKit/DataSources/VirtualBuddyFetcher.swift index 54efdb3b..9e836dc3 100644 --- a/Examples/BushelCloud/Sources/BushelCloudKit/DataSources/VirtualBuddyFetcher.swift +++ b/Examples/BushelCloud/Sources/BushelCloudKit/DataSources/VirtualBuddyFetcher.swift @@ -27,13 +27,13 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import BushelFoundation -import BushelUtilities -import BushelVirtualBuddy -import Foundation +internal import BushelFoundation +internal import BushelUtilities +internal import BushelVirtualBuddy +internal import Foundation #if canImport(FoundationNetworking) - import FoundationNetworking + internal import FoundationNetworking #endif /// Fetcher for enriching restore images with VirtualBuddy TSS signing status diff --git a/Examples/BushelCloud/Sources/BushelCloudKit/DataSources/XcodeReleasesFetcher.swift b/Examples/BushelCloud/Sources/BushelCloudKit/DataSources/XcodeReleasesFetcher.swift index 8e719eb3..b6681909 100644 --- a/Examples/BushelCloud/Sources/BushelCloudKit/DataSources/XcodeReleasesFetcher.swift +++ b/Examples/BushelCloud/Sources/BushelCloudKit/DataSources/XcodeReleasesFetcher.swift @@ -29,15 +29,15 @@ public import BushelFoundation public import BushelLogging -import Foundation -import Logging +internal import Foundation +internal import Logging #if canImport(FelinePineSwift) - import FelinePineSwift + internal import FelinePineSwift #endif #if canImport(FoundationNetworking) - import FoundationNetworking + internal import FoundationNetworking #endif /// Fetcher for Xcode releases from xcodereleases.com JSON API diff --git a/Examples/BushelCloud/Sources/BushelCloudKit/Utilities/ConsoleOutput.swift b/Examples/BushelCloud/Sources/BushelCloudKit/Utilities/ConsoleOutput.swift index bad03b68..d6f7c8aa 100644 --- a/Examples/BushelCloud/Sources/BushelCloudKit/Utilities/ConsoleOutput.swift +++ b/Examples/BushelCloud/Sources/BushelCloudKit/Utilities/ConsoleOutput.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Synchronization +internal import Foundation +internal import Synchronization /// Console output control for CLI interface /// diff --git a/Examples/BushelCloud/Sources/ConfigKeyKit/ConfigKey.swift b/Examples/BushelCloud/Sources/ConfigKeyKit/ConfigKey.swift index cac3ef08..b497316d 100644 --- a/Examples/BushelCloud/Sources/ConfigKeyKit/ConfigKey.swift +++ b/Examples/BushelCloud/Sources/ConfigKeyKit/ConfigKey.swift @@ -27,7 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation +internal import Foundation // MARK: - Generic Configuration Key diff --git a/Examples/BushelCloud/Sources/ConfigKeyKit/ConfigurationKey.swift b/Examples/BushelCloud/Sources/ConfigKeyKit/ConfigurationKey.swift index 341a110f..28a3e51e 100644 --- a/Examples/BushelCloud/Sources/ConfigKeyKit/ConfigurationKey.swift +++ b/Examples/BushelCloud/Sources/ConfigKeyKit/ConfigurationKey.swift @@ -27,7 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation +internal import Foundation // MARK: - Configuration Key Source diff --git a/Examples/BushelCloud/Sources/ConfigKeyKit/OptionalConfigKey.swift b/Examples/BushelCloud/Sources/ConfigKeyKit/OptionalConfigKey.swift index 8e32aaec..5b818e50 100644 --- a/Examples/BushelCloud/Sources/ConfigKeyKit/OptionalConfigKey.swift +++ b/Examples/BushelCloud/Sources/ConfigKeyKit/OptionalConfigKey.swift @@ -27,7 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation +internal import Foundation // MARK: - Optional Configuration Key diff --git a/Examples/BushelCloud/Tests/BushelCloudKitTests/CloudKit/MockCloudKitServiceTests.swift b/Examples/BushelCloud/Tests/BushelCloudKitTests/CloudKit/MockCloudKitServiceTests.swift index 94f3138d..4898020c 100644 --- a/Examples/BushelCloud/Tests/BushelCloudKitTests/CloudKit/MockCloudKitServiceTests.swift +++ b/Examples/BushelCloud/Tests/BushelCloudKitTests/CloudKit/MockCloudKitServiceTests.swift @@ -27,10 +27,10 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import BushelFoundation -import Foundation -import MistKit -import Testing +internal import BushelFoundation +internal import Foundation +internal import MistKit +internal import Testing @testable import BushelCloudKit diff --git a/Examples/BushelCloud/Tests/BushelCloudKitTests/CloudKit/PEMValidatorTests.swift b/Examples/BushelCloud/Tests/BushelCloudKitTests/CloudKit/PEMValidatorTests.swift index 08a7a6fd..1bbed1d0 100644 --- a/Examples/BushelCloud/Tests/BushelCloudKitTests/CloudKit/PEMValidatorTests.swift +++ b/Examples/BushelCloud/Tests/BushelCloudKitTests/CloudKit/PEMValidatorTests.swift @@ -27,7 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Testing +internal import Testing @testable import BushelCloudKit diff --git a/Examples/BushelCloud/Tests/BushelCloudKitTests/Configuration/ConfigurationLoaderTests.swift b/Examples/BushelCloud/Tests/BushelCloudKitTests/Configuration/ConfigurationLoaderTests.swift index c1315836..a5d8eaee 100644 --- a/Examples/BushelCloud/Tests/BushelCloudKitTests/Configuration/ConfigurationLoaderTests.swift +++ b/Examples/BushelCloud/Tests/BushelCloudKitTests/Configuration/ConfigurationLoaderTests.swift @@ -5,10 +5,10 @@ // Comprehensive tests for ConfigurationLoader // -import Configuration -import Foundation -import MistKit -import Testing +internal import Configuration +internal import Foundation +internal import MistKit +internal import Testing @testable import BushelCloudKit @testable import BushelFoundation diff --git a/Examples/BushelCloud/Tests/BushelCloudKitTests/Configuration/FetchConfigurationTests.swift b/Examples/BushelCloud/Tests/BushelCloudKitTests/Configuration/FetchConfigurationTests.swift index 578751cf..7e985096 100644 --- a/Examples/BushelCloud/Tests/BushelCloudKitTests/Configuration/FetchConfigurationTests.swift +++ b/Examples/BushelCloud/Tests/BushelCloudKitTests/Configuration/FetchConfigurationTests.swift @@ -6,8 +6,8 @@ // Copyright © 2025 BrightDigit. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import BushelFoundation diff --git a/Examples/BushelCloud/Tests/BushelCloudKitTests/DataSources/MockAppleDBFetcherTests.swift b/Examples/BushelCloud/Tests/BushelCloudKitTests/DataSources/MockAppleDBFetcherTests.swift index e8bae23b..bb0fb7e5 100644 --- a/Examples/BushelCloud/Tests/BushelCloudKitTests/DataSources/MockAppleDBFetcherTests.swift +++ b/Examples/BushelCloud/Tests/BushelCloudKitTests/DataSources/MockAppleDBFetcherTests.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import BushelFoundation diff --git a/Examples/BushelCloud/Tests/BushelCloudKitTests/DataSources/MockIPSWFetcherTests.swift b/Examples/BushelCloud/Tests/BushelCloudKitTests/DataSources/MockIPSWFetcherTests.swift index c3e66c73..8b2f5de6 100644 --- a/Examples/BushelCloud/Tests/BushelCloudKitTests/DataSources/MockIPSWFetcherTests.swift +++ b/Examples/BushelCloud/Tests/BushelCloudKitTests/DataSources/MockIPSWFetcherTests.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import BushelFoundation diff --git a/Examples/BushelCloud/Tests/BushelCloudKitTests/DataSources/MockMESUFetcherTests.swift b/Examples/BushelCloud/Tests/BushelCloudKitTests/DataSources/MockMESUFetcherTests.swift index 2bf31de9..8ada1321 100644 --- a/Examples/BushelCloud/Tests/BushelCloudKitTests/DataSources/MockMESUFetcherTests.swift +++ b/Examples/BushelCloud/Tests/BushelCloudKitTests/DataSources/MockMESUFetcherTests.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import BushelFoundation diff --git a/Examples/BushelCloud/Tests/BushelCloudKitTests/DataSources/MockSwiftVersionFetcherTests.swift b/Examples/BushelCloud/Tests/BushelCloudKitTests/DataSources/MockSwiftVersionFetcherTests.swift index 54e22b52..f15ad5a6 100644 --- a/Examples/BushelCloud/Tests/BushelCloudKitTests/DataSources/MockSwiftVersionFetcherTests.swift +++ b/Examples/BushelCloud/Tests/BushelCloudKitTests/DataSources/MockSwiftVersionFetcherTests.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import BushelFoundation diff --git a/Examples/BushelCloud/Tests/BushelCloudKitTests/DataSources/MockXcodeReleasesFetcherTests.swift b/Examples/BushelCloud/Tests/BushelCloudKitTests/DataSources/MockXcodeReleasesFetcherTests.swift index 187f673f..be93db84 100644 --- a/Examples/BushelCloud/Tests/BushelCloudKitTests/DataSources/MockXcodeReleasesFetcherTests.swift +++ b/Examples/BushelCloud/Tests/BushelCloudKitTests/DataSources/MockXcodeReleasesFetcherTests.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import BushelFoundation diff --git a/Examples/BushelCloud/Tests/BushelCloudKitTests/DataSources/RestoreImageDeduplicationTests.swift b/Examples/BushelCloud/Tests/BushelCloudKitTests/DataSources/RestoreImageDeduplicationTests.swift index d0be214b..bbafacaa 100644 --- a/Examples/BushelCloud/Tests/BushelCloudKitTests/DataSources/RestoreImageDeduplicationTests.swift +++ b/Examples/BushelCloud/Tests/BushelCloudKitTests/DataSources/RestoreImageDeduplicationTests.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import BushelCloudKit @testable import BushelFoundation diff --git a/Examples/BushelCloud/Tests/BushelCloudKitTests/DataSources/RestoreImageMergeTests.swift b/Examples/BushelCloud/Tests/BushelCloudKitTests/DataSources/RestoreImageMergeTests.swift index c7432286..326bffef 100644 --- a/Examples/BushelCloud/Tests/BushelCloudKitTests/DataSources/RestoreImageMergeTests.swift +++ b/Examples/BushelCloud/Tests/BushelCloudKitTests/DataSources/RestoreImageMergeTests.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import BushelCloudKit @testable import BushelFoundation diff --git a/Examples/BushelCloud/Tests/BushelCloudKitTests/DataSources/SwiftVersionDeduplicationTests.swift b/Examples/BushelCloud/Tests/BushelCloudKitTests/DataSources/SwiftVersionDeduplicationTests.swift index ca803c67..17b86201 100644 --- a/Examples/BushelCloud/Tests/BushelCloudKitTests/DataSources/SwiftVersionDeduplicationTests.swift +++ b/Examples/BushelCloud/Tests/BushelCloudKitTests/DataSources/SwiftVersionDeduplicationTests.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import BushelCloudKit @testable import BushelFoundation diff --git a/Examples/BushelCloud/Tests/BushelCloudKitTests/DataSources/VirtualBuddyFetcherTests.swift b/Examples/BushelCloud/Tests/BushelCloudKitTests/DataSources/VirtualBuddyFetcherTests.swift index 34732c91..4ae97c5b 100644 --- a/Examples/BushelCloud/Tests/BushelCloudKitTests/DataSources/VirtualBuddyFetcherTests.swift +++ b/Examples/BushelCloud/Tests/BushelCloudKitTests/DataSources/VirtualBuddyFetcherTests.swift @@ -27,14 +27,14 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import BushelCloudKit @testable import BushelFoundation #if canImport(FoundationNetworking) - import FoundationNetworking + internal import FoundationNetworking #endif /// All VirtualBuddy tests wrapped in a serialized suite to prevent mock handler conflicts diff --git a/Examples/BushelCloud/Tests/BushelCloudKitTests/DataSources/XcodeVersionDeduplicationTests.swift b/Examples/BushelCloud/Tests/BushelCloudKitTests/DataSources/XcodeVersionDeduplicationTests.swift index 4fb2462c..65087e83 100644 --- a/Examples/BushelCloud/Tests/BushelCloudKitTests/DataSources/XcodeVersionDeduplicationTests.swift +++ b/Examples/BushelCloud/Tests/BushelCloudKitTests/DataSources/XcodeVersionDeduplicationTests.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import BushelCloudKit @testable import BushelFoundation diff --git a/Examples/BushelCloud/Tests/BushelCloudKitTests/DataSources/XcodeVersionReferenceResolutionTests.swift b/Examples/BushelCloud/Tests/BushelCloudKitTests/DataSources/XcodeVersionReferenceResolutionTests.swift index 4619641b..e2f5e5ae 100644 --- a/Examples/BushelCloud/Tests/BushelCloudKitTests/DataSources/XcodeVersionReferenceResolutionTests.swift +++ b/Examples/BushelCloud/Tests/BushelCloudKitTests/DataSources/XcodeVersionReferenceResolutionTests.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import BushelCloudKit @testable import BushelFoundation diff --git a/Examples/BushelCloud/Tests/BushelCloudKitTests/ErrorHandling/AuthenticationErrorHandlingTests.swift b/Examples/BushelCloud/Tests/BushelCloudKitTests/ErrorHandling/AuthenticationErrorHandlingTests.swift index 9aeadb43..e5b86560 100644 --- a/Examples/BushelCloud/Tests/BushelCloudKitTests/ErrorHandling/AuthenticationErrorHandlingTests.swift +++ b/Examples/BushelCloud/Tests/BushelCloudKitTests/ErrorHandling/AuthenticationErrorHandlingTests.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing // MARK: - Authentication Error Handling Tests diff --git a/Examples/BushelCloud/Tests/BushelCloudKitTests/ErrorHandling/CloudKitErrorHandlingTests.swift b/Examples/BushelCloud/Tests/BushelCloudKitTests/ErrorHandling/CloudKitErrorHandlingTests.swift index 968e0732..4236b9e3 100644 --- a/Examples/BushelCloud/Tests/BushelCloudKitTests/ErrorHandling/CloudKitErrorHandlingTests.swift +++ b/Examples/BushelCloud/Tests/BushelCloudKitTests/ErrorHandling/CloudKitErrorHandlingTests.swift @@ -27,9 +27,9 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import MistKit -import Testing +internal import Foundation +internal import MistKit +internal import Testing @testable import BushelCloudKit diff --git a/Examples/BushelCloud/Tests/BushelCloudKitTests/ErrorHandling/GracefulDegradationTests.swift b/Examples/BushelCloud/Tests/BushelCloudKitTests/ErrorHandling/GracefulDegradationTests.swift index aba5179e..f09f4679 100644 --- a/Examples/BushelCloud/Tests/BushelCloudKitTests/ErrorHandling/GracefulDegradationTests.swift +++ b/Examples/BushelCloud/Tests/BushelCloudKitTests/ErrorHandling/GracefulDegradationTests.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing // MARK: - Graceful Degradation Tests diff --git a/Examples/BushelCloud/Tests/BushelCloudKitTests/ErrorHandling/NetworkErrorHandlingTests.swift b/Examples/BushelCloud/Tests/BushelCloudKitTests/ErrorHandling/NetworkErrorHandlingTests.swift index 3163b909..7d0cc87e 100644 --- a/Examples/BushelCloud/Tests/BushelCloudKitTests/ErrorHandling/NetworkErrorHandlingTests.swift +++ b/Examples/BushelCloud/Tests/BushelCloudKitTests/ErrorHandling/NetworkErrorHandlingTests.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing // MARK: - Network Error Handling Tests diff --git a/Examples/BushelCloud/Tests/BushelCloudKitTests/Extensions/FieldValueURLTests.swift b/Examples/BushelCloud/Tests/BushelCloudKitTests/Extensions/FieldValueURLTests.swift index 2040b78d..7ac8b34e 100644 --- a/Examples/BushelCloud/Tests/BushelCloudKitTests/Extensions/FieldValueURLTests.swift +++ b/Examples/BushelCloud/Tests/BushelCloudKitTests/Extensions/FieldValueURLTests.swift @@ -5,9 +5,9 @@ // Created by Claude Code // -import Foundation -import MistKit -import Testing +internal import Foundation +internal import MistKit +internal import Testing @testable import BushelCloudKit diff --git a/Examples/BushelCloud/Tests/BushelCloudKitTests/Mocks/MockAppleDBFetcher.swift b/Examples/BushelCloud/Tests/BushelCloudKitTests/Mocks/MockAppleDBFetcher.swift index dbca9a82..965cb271 100644 --- a/Examples/BushelCloud/Tests/BushelCloudKitTests/Mocks/MockAppleDBFetcher.swift +++ b/Examples/BushelCloud/Tests/BushelCloudKitTests/Mocks/MockAppleDBFetcher.swift @@ -27,7 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation +internal import Foundation @testable import BushelFoundation diff --git a/Examples/BushelCloud/Tests/BushelCloudKitTests/Mocks/MockCloudKitService.swift b/Examples/BushelCloud/Tests/BushelCloudKitTests/Mocks/MockCloudKitService.swift index 7329f425..831a967c 100644 --- a/Examples/BushelCloud/Tests/BushelCloudKitTests/Mocks/MockCloudKitService.swift +++ b/Examples/BushelCloud/Tests/BushelCloudKitTests/Mocks/MockCloudKitService.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import MistKit +internal import Foundation +internal import MistKit // MARK: - Mock CloudKit Errors diff --git a/Examples/BushelCloud/Tests/BushelCloudKitTests/Mocks/MockFetcherError.swift b/Examples/BushelCloud/Tests/BushelCloudKitTests/Mocks/MockFetcherError.swift index 0f56c232..42bdd97b 100644 --- a/Examples/BushelCloud/Tests/BushelCloudKitTests/Mocks/MockFetcherError.swift +++ b/Examples/BushelCloud/Tests/BushelCloudKitTests/Mocks/MockFetcherError.swift @@ -27,7 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation +internal import Foundation internal enum MockFetcherError: Error, Sendable { case networkError(String) diff --git a/Examples/BushelCloud/Tests/BushelCloudKitTests/Mocks/MockIPSWFetcher.swift b/Examples/BushelCloud/Tests/BushelCloudKitTests/Mocks/MockIPSWFetcher.swift index 73cbb943..2e66edf3 100644 --- a/Examples/BushelCloud/Tests/BushelCloudKitTests/Mocks/MockIPSWFetcher.swift +++ b/Examples/BushelCloud/Tests/BushelCloudKitTests/Mocks/MockIPSWFetcher.swift @@ -27,7 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation +internal import Foundation @testable import BushelFoundation diff --git a/Examples/BushelCloud/Tests/BushelCloudKitTests/Mocks/MockMESUFetcher.swift b/Examples/BushelCloud/Tests/BushelCloudKitTests/Mocks/MockMESUFetcher.swift index 93611dbb..025c213d 100644 --- a/Examples/BushelCloud/Tests/BushelCloudKitTests/Mocks/MockMESUFetcher.swift +++ b/Examples/BushelCloud/Tests/BushelCloudKitTests/Mocks/MockMESUFetcher.swift @@ -27,7 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation +internal import Foundation @testable import BushelFoundation diff --git a/Examples/BushelCloud/Tests/BushelCloudKitTests/Mocks/MockSwiftVersionFetcher.swift b/Examples/BushelCloud/Tests/BushelCloudKitTests/Mocks/MockSwiftVersionFetcher.swift index 2f28cde1..d6b97101 100644 --- a/Examples/BushelCloud/Tests/BushelCloudKitTests/Mocks/MockSwiftVersionFetcher.swift +++ b/Examples/BushelCloud/Tests/BushelCloudKitTests/Mocks/MockSwiftVersionFetcher.swift @@ -27,7 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation +internal import Foundation @testable import BushelFoundation diff --git a/Examples/BushelCloud/Tests/BushelCloudKitTests/Mocks/MockURLProtocol.swift b/Examples/BushelCloud/Tests/BushelCloudKitTests/Mocks/MockURLProtocol.swift index 368524fe..2c4d791c 100644 --- a/Examples/BushelCloud/Tests/BushelCloudKitTests/Mocks/MockURLProtocol.swift +++ b/Examples/BushelCloud/Tests/BushelCloudKitTests/Mocks/MockURLProtocol.swift @@ -30,7 +30,7 @@ public import Foundation #if canImport(FoundationNetworking) - import FoundationNetworking + internal import FoundationNetworking #endif /// Mock URLProtocol for intercepting and simulating HTTP requests in tests diff --git a/Examples/BushelCloud/Tests/BushelCloudKitTests/Mocks/MockXcodeReleasesFetcher.swift b/Examples/BushelCloud/Tests/BushelCloudKitTests/Mocks/MockXcodeReleasesFetcher.swift index 1b018b2b..4621ca5e 100644 --- a/Examples/BushelCloud/Tests/BushelCloudKitTests/Mocks/MockXcodeReleasesFetcher.swift +++ b/Examples/BushelCloud/Tests/BushelCloudKitTests/Mocks/MockXcodeReleasesFetcher.swift @@ -27,7 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation +internal import Foundation @testable import BushelFoundation diff --git a/Examples/BushelCloud/Tests/BushelCloudKitTests/Models/DataSourceMetadataTests.swift b/Examples/BushelCloud/Tests/BushelCloudKitTests/Models/DataSourceMetadataTests.swift index 034c7f93..d069d2a8 100644 --- a/Examples/BushelCloud/Tests/BushelCloudKitTests/Models/DataSourceMetadataTests.swift +++ b/Examples/BushelCloud/Tests/BushelCloudKitTests/Models/DataSourceMetadataTests.swift @@ -6,9 +6,9 @@ // Copyright © 2025 BrightDigit. // -import BushelFoundation -import MistKit -import Testing +internal import BushelFoundation +internal import MistKit +internal import Testing @testable import BushelCloudKit diff --git a/Examples/BushelCloud/Tests/BushelCloudKitTests/Models/RestoreImageRecordTests.swift b/Examples/BushelCloud/Tests/BushelCloudKitTests/Models/RestoreImageRecordTests.swift index fcb14180..440788e7 100644 --- a/Examples/BushelCloud/Tests/BushelCloudKitTests/Models/RestoreImageRecordTests.swift +++ b/Examples/BushelCloud/Tests/BushelCloudKitTests/Models/RestoreImageRecordTests.swift @@ -27,9 +27,9 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import MistKit -import Testing +internal import Foundation +internal import MistKit +internal import Testing @testable import BushelCloudKit @testable import BushelFoundation diff --git a/Examples/BushelCloud/Tests/BushelCloudKitTests/Models/SwiftVersionRecordTests.swift b/Examples/BushelCloud/Tests/BushelCloudKitTests/Models/SwiftVersionRecordTests.swift index 03657a35..c12e1c71 100644 --- a/Examples/BushelCloud/Tests/BushelCloudKitTests/Models/SwiftVersionRecordTests.swift +++ b/Examples/BushelCloud/Tests/BushelCloudKitTests/Models/SwiftVersionRecordTests.swift @@ -6,8 +6,8 @@ // Copyright © 2025 BrightDigit. // -import MistKit -import Testing +internal import MistKit +internal import Testing @testable import BushelCloudKit @testable import BushelFoundation diff --git a/Examples/BushelCloud/Tests/BushelCloudKitTests/Models/XcodeVersionRecordTests.swift b/Examples/BushelCloud/Tests/BushelCloudKitTests/Models/XcodeVersionRecordTests.swift index 709754b5..43c786e8 100644 --- a/Examples/BushelCloud/Tests/BushelCloudKitTests/Models/XcodeVersionRecordTests.swift +++ b/Examples/BushelCloud/Tests/BushelCloudKitTests/Models/XcodeVersionRecordTests.swift @@ -6,8 +6,8 @@ // Copyright © 2025 BrightDigit. // -import MistKit -import Testing +internal import MistKit +internal import Testing @testable import BushelCloudKit @testable import BushelFoundation diff --git a/Examples/BushelCloud/Tests/BushelCloudKitTests/Utilities/FieldValue+Assertions.swift b/Examples/BushelCloud/Tests/BushelCloudKitTests/Utilities/FieldValue+Assertions.swift index 822cdc5f..57806116 100644 --- a/Examples/BushelCloud/Tests/BushelCloudKitTests/Utilities/FieldValue+Assertions.swift +++ b/Examples/BushelCloud/Tests/BushelCloudKitTests/Utilities/FieldValue+Assertions.swift @@ -29,7 +29,7 @@ public import Foundation public import MistKit -import Testing +internal import Testing /// Custom assertions for FieldValue comparisons extension FieldValue { diff --git a/Examples/BushelCloud/Tests/ConfigKeyKitTests/ConfigKeySourceTests.swift b/Examples/BushelCloud/Tests/ConfigKeyKitTests/ConfigKeySourceTests.swift index e3dacfdd..ce45cdea 100644 --- a/Examples/BushelCloud/Tests/ConfigKeyKitTests/ConfigKeySourceTests.swift +++ b/Examples/BushelCloud/Tests/ConfigKeyKitTests/ConfigKeySourceTests.swift @@ -5,7 +5,7 @@ // Tests for ConfigKeySource enum // -import Testing +internal import Testing @testable import ConfigKeyKit diff --git a/Examples/BushelCloud/Tests/ConfigKeyKitTests/ConfigKeyTests.swift b/Examples/BushelCloud/Tests/ConfigKeyKitTests/ConfigKeyTests.swift index 512510d8..3c13267d 100644 --- a/Examples/BushelCloud/Tests/ConfigKeyKitTests/ConfigKeyTests.swift +++ b/Examples/BushelCloud/Tests/ConfigKeyKitTests/ConfigKeyTests.swift @@ -5,7 +5,7 @@ // Tests for ConfigKey configuration // -import Testing +internal import Testing @testable import ConfigKeyKit diff --git a/Examples/BushelCloud/Tests/ConfigKeyKitTests/NamingStyleTests.swift b/Examples/BushelCloud/Tests/ConfigKeyKitTests/NamingStyleTests.swift index e45dca0a..001a65d7 100644 --- a/Examples/BushelCloud/Tests/ConfigKeyKitTests/NamingStyleTests.swift +++ b/Examples/BushelCloud/Tests/ConfigKeyKitTests/NamingStyleTests.swift @@ -5,7 +5,7 @@ // Tests for naming style transformations // -import Testing +internal import Testing @testable import ConfigKeyKit diff --git a/Examples/BushelCloud/Tests/ConfigKeyKitTests/OptionalConfigKeyTests.swift b/Examples/BushelCloud/Tests/ConfigKeyKitTests/OptionalConfigKeyTests.swift index 3daac28f..425157b1 100644 --- a/Examples/BushelCloud/Tests/ConfigKeyKitTests/OptionalConfigKeyTests.swift +++ b/Examples/BushelCloud/Tests/ConfigKeyKitTests/OptionalConfigKeyTests.swift @@ -5,7 +5,7 @@ // Tests for OptionalConfigKey configuration // -import Testing +internal import Testing @testable import ConfigKeyKit diff --git a/Examples/CelestraCloud/Sources/CelestraCloud/Celestra.swift b/Examples/CelestraCloud/Sources/CelestraCloud/Celestra.swift index 8d0150ff..1d25ed5f 100644 --- a/Examples/CelestraCloud/Sources/CelestraCloud/Celestra.swift +++ b/Examples/CelestraCloud/Sources/CelestraCloud/Celestra.swift @@ -27,7 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation +internal import Foundation @main internal enum Celestra { diff --git a/Examples/CelestraCloud/Sources/CelestraCloud/Commands/AddFeedCommand.swift b/Examples/CelestraCloud/Sources/CelestraCloud/Commands/AddFeedCommand.swift index 90438e7d..b3cb333c 100644 --- a/Examples/CelestraCloud/Sources/CelestraCloud/Commands/AddFeedCommand.swift +++ b/Examples/CelestraCloud/Sources/CelestraCloud/Commands/AddFeedCommand.swift @@ -27,10 +27,10 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import CelestraCloudKit -import CelestraKit -import Foundation -import MistKit +internal import CelestraCloudKit +internal import CelestraKit +internal import Foundation +internal import MistKit // MARK: - Main Type diff --git a/Examples/CelestraCloud/Sources/CelestraCloud/Commands/ClearCommand.swift b/Examples/CelestraCloud/Sources/CelestraCloud/Commands/ClearCommand.swift index b122ec03..9af2eba4 100644 --- a/Examples/CelestraCloud/Sources/CelestraCloud/Commands/ClearCommand.swift +++ b/Examples/CelestraCloud/Sources/CelestraCloud/Commands/ClearCommand.swift @@ -27,10 +27,10 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import CelestraCloudKit -import CelestraKit -import Foundation -import MistKit +internal import CelestraCloudKit +internal import CelestraKit +internal import Foundation +internal import MistKit internal enum ClearCommand { internal static func run(args: [String]) async throws { diff --git a/Examples/CelestraCloud/Sources/CelestraCloud/Commands/UpdateCommand+Reporting.swift b/Examples/CelestraCloud/Sources/CelestraCloud/Commands/UpdateCommand+Reporting.swift index fef95ff9..64ee2d46 100644 --- a/Examples/CelestraCloud/Sources/CelestraCloud/Commands/UpdateCommand+Reporting.swift +++ b/Examples/CelestraCloud/Sources/CelestraCloud/Commands/UpdateCommand+Reporting.swift @@ -27,10 +27,10 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import CelestraCloudKit -import CelestraKit -import Foundation -import MistKit +internal import CelestraCloudKit +internal import CelestraKit +internal import Foundation +internal import MistKit extension UpdateCommand { internal static func createFeedResult( diff --git a/Examples/CelestraCloud/Sources/CelestraCloud/Commands/UpdateCommand.swift b/Examples/CelestraCloud/Sources/CelestraCloud/Commands/UpdateCommand.swift index ac3f48ef..1336e98e 100644 --- a/Examples/CelestraCloud/Sources/CelestraCloud/Commands/UpdateCommand.swift +++ b/Examples/CelestraCloud/Sources/CelestraCloud/Commands/UpdateCommand.swift @@ -27,10 +27,10 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import CelestraCloudKit -import CelestraKit -import Foundation -import MistKit +internal import CelestraCloudKit +internal import CelestraKit +internal import Foundation +internal import MistKit internal enum UpdateCommand { internal static func run() async throws { diff --git a/Examples/CelestraCloud/Sources/CelestraCloud/Commands/UpdateCommandError.swift b/Examples/CelestraCloud/Sources/CelestraCloud/Commands/UpdateCommandError.swift index 5de33763..f7710066 100644 --- a/Examples/CelestraCloud/Sources/CelestraCloud/Commands/UpdateCommandError.swift +++ b/Examples/CelestraCloud/Sources/CelestraCloud/Commands/UpdateCommandError.swift @@ -27,7 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation +internal import Foundation /// Errors specific to feed update operations internal struct UpdateCommandError: LocalizedError { diff --git a/Examples/CelestraCloud/Sources/CelestraCloud/Commands/UpdateSummary.swift b/Examples/CelestraCloud/Sources/CelestraCloud/Commands/UpdateSummary.swift index 8ade5d61..ff4a7e2f 100644 --- a/Examples/CelestraCloud/Sources/CelestraCloud/Commands/UpdateSummary.swift +++ b/Examples/CelestraCloud/Sources/CelestraCloud/Commands/UpdateSummary.swift @@ -27,7 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import CelestraCloudKit +internal import CelestraCloudKit /// Tracks update operation statistics internal struct UpdateSummary { diff --git a/Examples/CelestraCloud/Sources/CelestraCloud/Services/FeedUpdateProcessor+Fetch.swift b/Examples/CelestraCloud/Sources/CelestraCloud/Services/FeedUpdateProcessor+Fetch.swift index 236c0605..c1664fe1 100644 --- a/Examples/CelestraCloud/Sources/CelestraCloud/Services/FeedUpdateProcessor+Fetch.swift +++ b/Examples/CelestraCloud/Sources/CelestraCloud/Services/FeedUpdateProcessor+Fetch.swift @@ -27,10 +27,10 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import CelestraCloudKit -import CelestraKit -import Foundation -import MistKit +internal import CelestraCloudKit +internal import CelestraKit +internal import Foundation +internal import MistKit extension FeedUpdateProcessor { internal func processSuccessfulFetch( diff --git a/Examples/CelestraCloud/Sources/CelestraCloud/Services/FeedUpdateProcessor.swift b/Examples/CelestraCloud/Sources/CelestraCloud/Services/FeedUpdateProcessor.swift index c880c58e..5153e5d7 100644 --- a/Examples/CelestraCloud/Sources/CelestraCloud/Services/FeedUpdateProcessor.swift +++ b/Examples/CelestraCloud/Sources/CelestraCloud/Services/FeedUpdateProcessor.swift @@ -27,10 +27,10 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import CelestraCloudKit -import CelestraKit -import Foundation -import MistKit +internal import CelestraCloudKit +internal import CelestraKit +internal import Foundation +internal import MistKit /// Processes individual feed updates internal struct FeedUpdateProcessor { diff --git a/Examples/CelestraCloud/Sources/CelestraCloudKit/Services/ArticleCloudKitService.swift b/Examples/CelestraCloud/Sources/CelestraCloudKit/Services/ArticleCloudKitService.swift index aeafbe16..afc2ed56 100644 --- a/Examples/CelestraCloud/Sources/CelestraCloudKit/Services/ArticleCloudKitService.swift +++ b/Examples/CelestraCloud/Sources/CelestraCloudKit/Services/ArticleCloudKitService.swift @@ -29,7 +29,7 @@ public import CelestraKit public import Foundation -import Logging +internal import Logging public import MistKit // swiftlint:disable file_length diff --git a/Examples/CelestraCloud/Sources/CelestraCloudKit/Services/FeedCloudKitService.swift b/Examples/CelestraCloud/Sources/CelestraCloudKit/Services/FeedCloudKitService.swift index 48ce59cb..7f62b556 100644 --- a/Examples/CelestraCloud/Sources/CelestraCloudKit/Services/FeedCloudKitService.swift +++ b/Examples/CelestraCloud/Sources/CelestraCloudKit/Services/FeedCloudKitService.swift @@ -29,7 +29,7 @@ public import CelestraKit public import Foundation -import Logging +internal import Logging public import MistKit /// Service for Feed-related CloudKit operations with dependency injection support diff --git a/Examples/CelestraCloud/Tests/CelestraCloudTests/Configuration/CloudKitConfigurationTests.swift b/Examples/CelestraCloud/Tests/CelestraCloudTests/Configuration/CloudKitConfigurationTests.swift index 22e7102d..d3825535 100644 --- a/Examples/CelestraCloud/Tests/CelestraCloudTests/Configuration/CloudKitConfigurationTests.swift +++ b/Examples/CelestraCloud/Tests/CelestraCloudTests/Configuration/CloudKitConfigurationTests.swift @@ -27,9 +27,9 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import MistKit -import Testing +internal import Foundation +internal import MistKit +internal import Testing @testable import CelestraCloudKit diff --git a/Examples/CelestraCloud/Tests/CelestraCloudTests/Configuration/UpdateCommandConfigurationTests.swift b/Examples/CelestraCloud/Tests/CelestraCloudTests/Configuration/UpdateCommandConfigurationTests.swift index d2f09cf8..9cc6188e 100644 --- a/Examples/CelestraCloud/Tests/CelestraCloudTests/Configuration/UpdateCommandConfigurationTests.swift +++ b/Examples/CelestraCloud/Tests/CelestraCloudTests/Configuration/UpdateCommandConfigurationTests.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import CelestraCloudKit diff --git a/Examples/CelestraCloud/Tests/CelestraCloudTests/Errors/CelestraErrorTests+Description.swift b/Examples/CelestraCloud/Tests/CelestraCloudTests/Errors/CelestraErrorTests+Description.swift index 9407fb20..5701e264 100644 --- a/Examples/CelestraCloud/Tests/CelestraCloudTests/Errors/CelestraErrorTests+Description.swift +++ b/Examples/CelestraCloud/Tests/CelestraCloudTests/Errors/CelestraErrorTests+Description.swift @@ -27,9 +27,9 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import MistKit -import Testing +internal import Foundation +internal import MistKit +internal import Testing @testable import CelestraCloudKit diff --git a/Examples/CelestraCloud/Tests/CelestraCloudTests/Errors/CelestraErrorTests+RecoverySuggestion.swift b/Examples/CelestraCloud/Tests/CelestraCloudTests/Errors/CelestraErrorTests+RecoverySuggestion.swift index 11d56073..cc968d6c 100644 --- a/Examples/CelestraCloud/Tests/CelestraCloudTests/Errors/CelestraErrorTests+RecoverySuggestion.swift +++ b/Examples/CelestraCloud/Tests/CelestraCloudTests/Errors/CelestraErrorTests+RecoverySuggestion.swift @@ -27,9 +27,9 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import MistKit -import Testing +internal import Foundation +internal import MistKit +internal import Testing @testable import CelestraCloudKit diff --git a/Examples/CelestraCloud/Tests/CelestraCloudTests/Errors/CelestraErrorTests.swift b/Examples/CelestraCloud/Tests/CelestraCloudTests/Errors/CelestraErrorTests.swift index 7f88fb58..0274d8d0 100644 --- a/Examples/CelestraCloud/Tests/CelestraCloudTests/Errors/CelestraErrorTests.swift +++ b/Examples/CelestraCloud/Tests/CelestraCloudTests/Errors/CelestraErrorTests.swift @@ -27,9 +27,9 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import MistKit -import Testing +internal import Foundation +internal import MistKit +internal import Testing @testable import CelestraCloudKit diff --git a/Examples/CelestraCloud/Tests/CelestraCloudTests/Errors/CloudKitConversionErrorTests.swift b/Examples/CelestraCloud/Tests/CelestraCloudTests/Errors/CloudKitConversionErrorTests.swift index 4515cbe4..114df916 100644 --- a/Examples/CelestraCloud/Tests/CelestraCloudTests/Errors/CloudKitConversionErrorTests.swift +++ b/Examples/CelestraCloud/Tests/CelestraCloudTests/Errors/CloudKitConversionErrorTests.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import CelestraCloudKit diff --git a/Examples/CelestraCloud/Tests/CelestraCloudTests/Extensions/ArticleConversion+FromCloudKit.swift b/Examples/CelestraCloud/Tests/CelestraCloudTests/Extensions/ArticleConversion+FromCloudKit.swift index dc8e3ee1..a08aa956 100644 --- a/Examples/CelestraCloud/Tests/CelestraCloudTests/Extensions/ArticleConversion+FromCloudKit.swift +++ b/Examples/CelestraCloud/Tests/CelestraCloudTests/Extensions/ArticleConversion+FromCloudKit.swift @@ -1,7 +1,7 @@ -import CelestraKit -import Foundation -import MistKit -import Testing +internal import CelestraKit +internal import Foundation +internal import MistKit +internal import Testing @testable import CelestraCloudKit diff --git a/Examples/CelestraCloud/Tests/CelestraCloudTests/Extensions/ArticleConversion+ToCloudKit.swift b/Examples/CelestraCloud/Tests/CelestraCloudTests/Extensions/ArticleConversion+ToCloudKit.swift index ef341bef..1a091f2d 100644 --- a/Examples/CelestraCloud/Tests/CelestraCloudTests/Extensions/ArticleConversion+ToCloudKit.swift +++ b/Examples/CelestraCloud/Tests/CelestraCloudTests/Extensions/ArticleConversion+ToCloudKit.swift @@ -1,7 +1,7 @@ -import CelestraKit -import Foundation -import MistKit -import Testing +internal import CelestraKit +internal import Foundation +internal import MistKit +internal import Testing @testable import CelestraCloudKit diff --git a/Examples/CelestraCloud/Tests/CelestraCloudTests/Extensions/FeedConversion+FromCloudKit.swift b/Examples/CelestraCloud/Tests/CelestraCloudTests/Extensions/FeedConversion+FromCloudKit.swift index e5abb96d..f6284f97 100644 --- a/Examples/CelestraCloud/Tests/CelestraCloudTests/Extensions/FeedConversion+FromCloudKit.swift +++ b/Examples/CelestraCloud/Tests/CelestraCloudTests/Extensions/FeedConversion+FromCloudKit.swift @@ -1,7 +1,7 @@ -import CelestraKit -import Foundation -import MistKit -import Testing +internal import CelestraKit +internal import Foundation +internal import MistKit +internal import Testing @testable import CelestraCloudKit diff --git a/Examples/CelestraCloud/Tests/CelestraCloudTests/Extensions/FeedConversion+RoundTrip.swift b/Examples/CelestraCloud/Tests/CelestraCloudTests/Extensions/FeedConversion+RoundTrip.swift index 018cb130..d3bdaa01 100644 --- a/Examples/CelestraCloud/Tests/CelestraCloudTests/Extensions/FeedConversion+RoundTrip.swift +++ b/Examples/CelestraCloud/Tests/CelestraCloudTests/Extensions/FeedConversion+RoundTrip.swift @@ -1,7 +1,7 @@ -import CelestraKit -import Foundation -import MistKit -import Testing +internal import CelestraKit +internal import Foundation +internal import MistKit +internal import Testing @testable import CelestraCloudKit diff --git a/Examples/CelestraCloud/Tests/CelestraCloudTests/Extensions/FeedConversion+ToCloudKit.swift b/Examples/CelestraCloud/Tests/CelestraCloudTests/Extensions/FeedConversion+ToCloudKit.swift index 61ff2a15..be3919f8 100644 --- a/Examples/CelestraCloud/Tests/CelestraCloudTests/Extensions/FeedConversion+ToCloudKit.swift +++ b/Examples/CelestraCloud/Tests/CelestraCloudTests/Extensions/FeedConversion+ToCloudKit.swift @@ -1,7 +1,7 @@ -import CelestraKit -import Foundation -import MistKit -import Testing +internal import CelestraKit +internal import Foundation +internal import MistKit +internal import Testing @testable import CelestraCloudKit diff --git a/Examples/CelestraCloud/Tests/CelestraCloudTests/Mocks/MockCloudKitRecordOperator.swift b/Examples/CelestraCloud/Tests/CelestraCloudTests/Mocks/MockCloudKitRecordOperator.swift index 09aac148..63049388 100644 --- a/Examples/CelestraCloud/Tests/CelestraCloudTests/Mocks/MockCloudKitRecordOperator.swift +++ b/Examples/CelestraCloud/Tests/CelestraCloudTests/Mocks/MockCloudKitRecordOperator.swift @@ -27,9 +27,9 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import MistKit -import Synchronization +internal import Foundation +internal import MistKit +internal import Synchronization @testable import CelestraCloudKit diff --git a/Examples/CelestraCloud/Tests/CelestraCloudTests/Models/BatchOperationResultTests.swift b/Examples/CelestraCloud/Tests/CelestraCloudTests/Models/BatchOperationResultTests.swift index 95b7daaf..3f70016c 100644 --- a/Examples/CelestraCloud/Tests/CelestraCloudTests/Models/BatchOperationResultTests.swift +++ b/Examples/CelestraCloud/Tests/CelestraCloudTests/Models/BatchOperationResultTests.swift @@ -1,7 +1,7 @@ -import CelestraKit -import Foundation -import MistKit -import Testing +internal import CelestraKit +internal import Foundation +internal import MistKit +internal import Testing @testable import CelestraCloudKit diff --git a/Examples/CelestraCloud/Tests/CelestraCloudTests/Services/ArticleCategorizer+Advanced.swift b/Examples/CelestraCloud/Tests/CelestraCloudTests/Services/ArticleCategorizer+Advanced.swift index 84b2a16b..830f95f4 100644 --- a/Examples/CelestraCloud/Tests/CelestraCloudTests/Services/ArticleCategorizer+Advanced.swift +++ b/Examples/CelestraCloud/Tests/CelestraCloudTests/Services/ArticleCategorizer+Advanced.swift @@ -27,9 +27,9 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import CelestraKit -import Foundation -import Testing +internal import CelestraKit +internal import Foundation +internal import Testing @testable import CelestraCloudKit diff --git a/Examples/CelestraCloud/Tests/CelestraCloudTests/Services/ArticleCategorizer+Basic.swift b/Examples/CelestraCloud/Tests/CelestraCloudTests/Services/ArticleCategorizer+Basic.swift index 58ac2d58..415ad4bb 100644 --- a/Examples/CelestraCloud/Tests/CelestraCloudTests/Services/ArticleCategorizer+Basic.swift +++ b/Examples/CelestraCloud/Tests/CelestraCloudTests/Services/ArticleCategorizer+Basic.swift @@ -27,9 +27,9 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import CelestraKit -import Foundation -import Testing +internal import CelestraKit +internal import Foundation +internal import Testing @testable import CelestraCloudKit diff --git a/Examples/CelestraCloud/Tests/CelestraCloudTests/Services/ArticleCloudKitService+Mutations.swift b/Examples/CelestraCloud/Tests/CelestraCloudTests/Services/ArticleCloudKitService+Mutations.swift index 9db8260a..0fabb3da 100644 --- a/Examples/CelestraCloud/Tests/CelestraCloudTests/Services/ArticleCloudKitService+Mutations.swift +++ b/Examples/CelestraCloud/Tests/CelestraCloudTests/Services/ArticleCloudKitService+Mutations.swift @@ -27,10 +27,10 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import CelestraKit -import Foundation -import MistKit -import Testing +internal import CelestraKit +internal import Foundation +internal import MistKit +internal import Testing @testable import CelestraCloudKit diff --git a/Examples/CelestraCloud/Tests/CelestraCloudTests/Services/ArticleCloudKitService+Query.swift b/Examples/CelestraCloud/Tests/CelestraCloudTests/Services/ArticleCloudKitService+Query.swift index 4ec2091c..3e080ee6 100644 --- a/Examples/CelestraCloud/Tests/CelestraCloudTests/Services/ArticleCloudKitService+Query.swift +++ b/Examples/CelestraCloud/Tests/CelestraCloudTests/Services/ArticleCloudKitService+Query.swift @@ -27,10 +27,10 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import CelestraKit -import Foundation -import MistKit -import Testing +internal import CelestraKit +internal import Foundation +internal import MistKit +internal import Testing @testable import CelestraCloudKit diff --git a/Examples/CelestraCloud/Tests/CelestraCloudTests/Services/FeedCloudKitService+CRUD.swift b/Examples/CelestraCloud/Tests/CelestraCloudTests/Services/FeedCloudKitService+CRUD.swift index 629b25e0..35af1533 100644 --- a/Examples/CelestraCloud/Tests/CelestraCloudTests/Services/FeedCloudKitService+CRUD.swift +++ b/Examples/CelestraCloud/Tests/CelestraCloudTests/Services/FeedCloudKitService+CRUD.swift @@ -27,10 +27,10 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import CelestraKit -import Foundation -import MistKit -import Testing +internal import CelestraKit +internal import Foundation +internal import MistKit +internal import Testing @testable import CelestraCloudKit diff --git a/Examples/CelestraCloud/Tests/CelestraCloudTests/Services/FeedCloudKitService+Query.swift b/Examples/CelestraCloud/Tests/CelestraCloudTests/Services/FeedCloudKitService+Query.swift index ab4e8366..e4f598a7 100644 --- a/Examples/CelestraCloud/Tests/CelestraCloudTests/Services/FeedCloudKitService+Query.swift +++ b/Examples/CelestraCloud/Tests/CelestraCloudTests/Services/FeedCloudKitService+Query.swift @@ -27,10 +27,10 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import CelestraKit -import Foundation -import MistKit -import Testing +internal import CelestraKit +internal import Foundation +internal import MistKit +internal import Testing @testable import CelestraCloudKit diff --git a/Examples/CelestraCloud/Tests/CelestraCloudTests/Services/FeedMetadataBuilder+Error.swift b/Examples/CelestraCloud/Tests/CelestraCloudTests/Services/FeedMetadataBuilder+Error.swift index 5d952872..8b2889a8 100644 --- a/Examples/CelestraCloud/Tests/CelestraCloudTests/Services/FeedMetadataBuilder+Error.swift +++ b/Examples/CelestraCloud/Tests/CelestraCloudTests/Services/FeedMetadataBuilder+Error.swift @@ -27,9 +27,9 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import CelestraKit -import Foundation -import Testing +internal import CelestraKit +internal import Foundation +internal import Testing @testable import CelestraCloudKit diff --git a/Examples/CelestraCloud/Tests/CelestraCloudTests/Services/FeedMetadataBuilder+NotModified.swift b/Examples/CelestraCloud/Tests/CelestraCloudTests/Services/FeedMetadataBuilder+NotModified.swift index fdadd0d4..65263546 100644 --- a/Examples/CelestraCloud/Tests/CelestraCloudTests/Services/FeedMetadataBuilder+NotModified.swift +++ b/Examples/CelestraCloud/Tests/CelestraCloudTests/Services/FeedMetadataBuilder+NotModified.swift @@ -27,9 +27,9 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import CelestraKit -import Foundation -import Testing +internal import CelestraKit +internal import Foundation +internal import Testing @testable import CelestraCloudKit diff --git a/Examples/CelestraCloud/Tests/CelestraCloudTests/Services/FeedMetadataBuilder+Success.swift b/Examples/CelestraCloud/Tests/CelestraCloudTests/Services/FeedMetadataBuilder+Success.swift index 749e1ca8..3af551ea 100644 --- a/Examples/CelestraCloud/Tests/CelestraCloudTests/Services/FeedMetadataBuilder+Success.swift +++ b/Examples/CelestraCloud/Tests/CelestraCloudTests/Services/FeedMetadataBuilder+Success.swift @@ -27,9 +27,9 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import CelestraKit -import Foundation -import Testing +internal import CelestraKit +internal import Foundation +internal import Testing @testable import CelestraCloudKit diff --git a/Examples/MistDemo/App/MistDemoApp.swift b/Examples/MistDemo/App/MistDemoApp.swift index ea52376a..564b1f2f 100644 --- a/Examples/MistDemo/App/MistDemoApp.swift +++ b/Examples/MistDemo/App/MistDemoApp.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import MistDemoApp -import SwiftUI +internal import MistDemoApp +internal import SwiftUI @main internal struct MistDemoAppMain: AppMain { diff --git a/Examples/MistDemo/Sources/ConfigKeyKit/Command.swift b/Examples/MistDemo/Sources/ConfigKeyKit/Command.swift index 89506fa7..f9baa1cc 100644 --- a/Examples/MistDemo/Sources/ConfigKeyKit/Command.swift +++ b/Examples/MistDemo/Sources/ConfigKeyKit/Command.swift @@ -27,7 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation +internal import Foundation /// Generic protocol for CLI commands using Swift Configuration public protocol Command: Sendable { diff --git a/Examples/MistDemo/Sources/ConfigKeyKit/CommandLineParser.swift b/Examples/MistDemo/Sources/ConfigKeyKit/CommandLineParser.swift index b5457109..1e03d791 100644 --- a/Examples/MistDemo/Sources/ConfigKeyKit/CommandLineParser.swift +++ b/Examples/MistDemo/Sources/ConfigKeyKit/CommandLineParser.swift @@ -27,7 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation +internal import Foundation /// Command line argument parser for Swift Configuration integration public struct CommandLineParser { diff --git a/Examples/MistDemo/Sources/ConfigKeyKit/CommandRegistry.swift b/Examples/MistDemo/Sources/ConfigKeyKit/CommandRegistry.swift index 1d93e29c..27e836e2 100644 --- a/Examples/MistDemo/Sources/ConfigKeyKit/CommandRegistry.swift +++ b/Examples/MistDemo/Sources/ConfigKeyKit/CommandRegistry.swift @@ -27,7 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation +internal import Foundation /// Actor-based registry for managing available commands public actor CommandRegistry { diff --git a/Examples/MistDemo/Sources/ConfigKeyKit/ConfigKey+Bool.swift b/Examples/MistDemo/Sources/ConfigKeyKit/ConfigKey+Bool.swift index d155f087..eb9420d3 100644 --- a/Examples/MistDemo/Sources/ConfigKeyKit/ConfigKey+Bool.swift +++ b/Examples/MistDemo/Sources/ConfigKeyKit/ConfigKey+Bool.swift @@ -27,7 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation +internal import Foundation // MARK: - Specialized Initializers for Booleans diff --git a/Examples/MistDemo/Sources/ConfigKeyKit/ConfigKey.swift b/Examples/MistDemo/Sources/ConfigKeyKit/ConfigKey.swift index 4d2a40fd..a6a278f6 100644 --- a/Examples/MistDemo/Sources/ConfigKeyKit/ConfigKey.swift +++ b/Examples/MistDemo/Sources/ConfigKeyKit/ConfigKey.swift @@ -27,7 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation +internal import Foundation // MARK: - Generic Configuration Key diff --git a/Examples/MistDemo/Sources/ConfigKeyKit/ConfigurationKey.swift b/Examples/MistDemo/Sources/ConfigKeyKit/ConfigurationKey.swift index a2f015da..70458be3 100644 --- a/Examples/MistDemo/Sources/ConfigKeyKit/ConfigurationKey.swift +++ b/Examples/MistDemo/Sources/ConfigKeyKit/ConfigurationKey.swift @@ -27,7 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation +internal import Foundation // MARK: - Configuration Key Protocol diff --git a/Examples/MistDemo/Sources/ConfigKeyKit/ConfigurationParseable.swift b/Examples/MistDemo/Sources/ConfigKeyKit/ConfigurationParseable.swift index fcaab74d..417b1b24 100644 --- a/Examples/MistDemo/Sources/ConfigKeyKit/ConfigurationParseable.swift +++ b/Examples/MistDemo/Sources/ConfigKeyKit/ConfigurationParseable.swift @@ -27,7 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation +internal import Foundation /// Protocol for configuration types that can parse themselves /// from command line arguments and environment variables. diff --git a/Examples/MistDemo/Sources/ConfigKeyKit/OptionalConfigKey.swift b/Examples/MistDemo/Sources/ConfigKeyKit/OptionalConfigKey.swift index 1525d6ec..0a80c71d 100644 --- a/Examples/MistDemo/Sources/ConfigKeyKit/OptionalConfigKey.swift +++ b/Examples/MistDemo/Sources/ConfigKeyKit/OptionalConfigKey.swift @@ -27,7 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation +internal import Foundation // MARK: - Optional Configuration Key diff --git a/Examples/MistDemo/Sources/ConfigKeyKit/StandardNamingStyle.swift b/Examples/MistDemo/Sources/ConfigKeyKit/StandardNamingStyle.swift index 3f38b542..5b626df8 100644 --- a/Examples/MistDemo/Sources/ConfigKeyKit/StandardNamingStyle.swift +++ b/Examples/MistDemo/Sources/ConfigKeyKit/StandardNamingStyle.swift @@ -27,7 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation +internal import Foundation /// Common naming styles for configuration keys public enum StandardNamingStyle: NamingStyle, Sendable { diff --git a/Examples/MistDemo/Sources/MistDemo/MistDemo.swift b/Examples/MistDemo/Sources/MistDemo/MistDemo.swift index 2aa28bd9..571186d9 100644 --- a/Examples/MistDemo/Sources/MistDemo/MistDemo.swift +++ b/Examples/MistDemo/Sources/MistDemo/MistDemo.swift @@ -27,7 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import MistDemoKit +internal import MistDemoKit @main internal enum MistDemo { diff --git a/Examples/MistDemo/Sources/MistDemoApp/Models/CKRecord+TypedField.swift b/Examples/MistDemo/Sources/MistDemoApp/Models/CKRecord+TypedField.swift index 6a099e18..eb8a1232 100644 --- a/Examples/MistDemo/Sources/MistDemoApp/Models/CKRecord+TypedField.swift +++ b/Examples/MistDemo/Sources/MistDemoApp/Models/CKRecord+TypedField.swift @@ -28,8 +28,8 @@ // #if canImport(CloudKit) - import CloudKit - import Foundation + internal import CloudKit + internal import Foundation extension CKRecord { /// Reads `field` from the record and casts it to `T`. diff --git a/Examples/MistDemo/Sources/MistDemoApp/Models/Note.swift b/Examples/MistDemo/Sources/MistDemoApp/Models/Note.swift index 1961a64a..fab146b6 100644 --- a/Examples/MistDemo/Sources/MistDemoApp/Models/Note.swift +++ b/Examples/MistDemo/Sources/MistDemoApp/Models/Note.swift @@ -28,9 +28,9 @@ // #if canImport(CloudKit) - import CloudKit - import Foundation - import MistDemoKit + internal import CloudKit + internal import Foundation + internal import MistDemoKit /// Note record, mirroring the `Note` type defined in `schema.ckdb`: /// diff --git a/Examples/MistDemo/Sources/MistDemoApp/Models/ZoneRow.swift b/Examples/MistDemo/Sources/MistDemoApp/Models/ZoneRow.swift index ac711fc5..f9648aa9 100644 --- a/Examples/MistDemo/Sources/MistDemoApp/Models/ZoneRow.swift +++ b/Examples/MistDemo/Sources/MistDemoApp/Models/ZoneRow.swift @@ -28,9 +28,9 @@ // #if canImport(CloudKit) - import CloudKit - import Foundation - import MistDemoKit + internal import CloudKit + internal import Foundation + internal import MistDemoKit /// Display-friendly snapshot of a CKRecordZone for the SwiftUI list. extension ZoneRow { diff --git a/Examples/MistDemo/Sources/MistDemoApp/Services/CKDatabase+WebAuthToken.swift b/Examples/MistDemo/Sources/MistDemoApp/Services/CKDatabase+WebAuthToken.swift index d34a79ec..632cc3aa 100644 --- a/Examples/MistDemo/Sources/MistDemoApp/Services/CKDatabase+WebAuthToken.swift +++ b/Examples/MistDemo/Sources/MistDemoApp/Services/CKDatabase+WebAuthToken.swift @@ -28,7 +28,7 @@ // #if canImport(CloudKit) - import CloudKit + internal import CloudKit extension CKDatabase { /// Capture a web-auth token via `CKFetchWebAuthTokenOperation` for the diff --git a/Examples/MistDemo/Sources/MistDemoApp/Services/CKDatabase.Scope+Demo.swift b/Examples/MistDemo/Sources/MistDemoApp/Services/CKDatabase.Scope+Demo.swift index 0be3b9b1..29d43615 100644 --- a/Examples/MistDemo/Sources/MistDemoApp/Services/CKDatabase.Scope+Demo.swift +++ b/Examples/MistDemo/Sources/MistDemoApp/Services/CKDatabase.Scope+Demo.swift @@ -28,7 +28,7 @@ // #if canImport(CloudKit) - import CloudKit + internal import CloudKit extension CKDatabase.Scope { /// Scopes exposed in the MistDemoApp picker. `.shared` is intentionally diff --git a/Examples/MistDemo/Sources/MistDemoApp/Services/CloudKitStore.swift b/Examples/MistDemo/Sources/MistDemoApp/Services/CloudKitStore.swift index 81c8e926..2c448d81 100644 --- a/Examples/MistDemo/Sources/MistDemoApp/Services/CloudKitStore.swift +++ b/Examples/MistDemo/Sources/MistDemoApp/Services/CloudKitStore.swift @@ -28,9 +28,9 @@ // #if canImport(CloudKit) - import CloudKit - import Foundation - import MistDemoKit + internal import CloudKit + internal import Foundation + internal import MistDemoKit public import Observation /// Observable source of truth for the MistDemo app's CloudKit state. diff --git a/Examples/MistDemo/Sources/MistDemoApp/Services/CloudKitStoreError.swift b/Examples/MistDemo/Sources/MistDemoApp/Services/CloudKitStoreError.swift index 4826fb52..373bada2 100644 --- a/Examples/MistDemo/Sources/MistDemoApp/Services/CloudKitStoreError.swift +++ b/Examples/MistDemo/Sources/MistDemoApp/Services/CloudKitStoreError.swift @@ -28,8 +28,8 @@ // #if canImport(CloudKit) - import Foundation - import MistDemoKit + internal import Foundation + internal import MistDemoKit /// Errors specific to `CloudKitStore` operations. internal enum CloudKitStoreError: Error, LocalizedError { diff --git a/Examples/MistDemo/Sources/MistDemoApp/Views/AccountView+Actions.swift b/Examples/MistDemo/Sources/MistDemoApp/Views/AccountView+Actions.swift index b228da76..f8c1040f 100644 --- a/Examples/MistDemo/Sources/MistDemoApp/Views/AccountView+Actions.swift +++ b/Examples/MistDemo/Sources/MistDemoApp/Views/AccountView+Actions.swift @@ -28,13 +28,13 @@ // #if canImport(SwiftUI) && canImport(CloudKit) - import CloudKit - import SwiftUI + internal import CloudKit + internal import SwiftUI #if canImport(AppKit) - import AppKit + internal import AppKit #elseif canImport(UIKit) - import UIKit + internal import UIKit #endif extension AccountView { diff --git a/Examples/MistDemo/Sources/MistDemoApp/Views/AccountView.swift b/Examples/MistDemo/Sources/MistDemoApp/Views/AccountView.swift index 8892fa41..5eff98c4 100644 --- a/Examples/MistDemo/Sources/MistDemoApp/Views/AccountView.swift +++ b/Examples/MistDemo/Sources/MistDemoApp/Views/AccountView.swift @@ -28,13 +28,13 @@ // #if canImport(SwiftUI) && canImport(CloudKit) - import CloudKit - import SwiftUI + internal import CloudKit + internal import SwiftUI #if canImport(AppKit) - import AppKit + internal import AppKit #elseif canImport(UIKit) - import UIKit + internal import UIKit #endif /// View showing the iCloud account status, the public/private database diff --git a/Examples/MistDemo/Sources/MistDemoApp/Views/DetailColumnRoot.swift b/Examples/MistDemo/Sources/MistDemoApp/Views/DetailColumnRoot.swift index dbea999a..82684084 100644 --- a/Examples/MistDemo/Sources/MistDemoApp/Views/DetailColumnRoot.swift +++ b/Examples/MistDemo/Sources/MistDemoApp/Views/DetailColumnRoot.swift @@ -28,7 +28,7 @@ // #if canImport(SwiftUI) - import SwiftUI + internal import SwiftUI /// Routes the sidebar selection to the appropriate detail view. internal struct DetailColumnRoot: View { diff --git a/Examples/MistDemo/Sources/MistDemoApp/Views/NoteEditView.swift b/Examples/MistDemo/Sources/MistDemoApp/Views/NoteEditView.swift index 19e74e1b..43d059df 100644 --- a/Examples/MistDemo/Sources/MistDemoApp/Views/NoteEditView.swift +++ b/Examples/MistDemo/Sources/MistDemoApp/Views/NoteEditView.swift @@ -28,9 +28,9 @@ // #if canImport(SwiftUI) && canImport(CloudKit) - import MistDemoKit - import SwiftUI - import UniformTypeIdentifiers + internal import MistDemoKit + internal import SwiftUI + internal import UniformTypeIdentifiers /// Sheet form for creating or editing a Note. The same view backs both flows; /// the `mode` value drives the title and which service method is called on save. diff --git a/Examples/MistDemo/Sources/MistDemoApp/Views/QueryView.swift b/Examples/MistDemo/Sources/MistDemoApp/Views/QueryView.swift index e5bdca8a..aafa886e 100644 --- a/Examples/MistDemo/Sources/MistDemoApp/Views/QueryView.swift +++ b/Examples/MistDemo/Sources/MistDemoApp/Views/QueryView.swift @@ -28,8 +28,8 @@ // #if canImport(SwiftUI) && canImport(CloudKit) - import MistDemoKit - import SwiftUI + internal import MistDemoKit + internal import SwiftUI /// View for querying Note records from CloudKit. internal struct QueryView: View { diff --git a/Examples/MistDemo/Sources/MistDemoApp/Views/RecordDetailView.swift b/Examples/MistDemo/Sources/MistDemoApp/Views/RecordDetailView.swift index e725b50e..856ab594 100644 --- a/Examples/MistDemo/Sources/MistDemoApp/Views/RecordDetailView.swift +++ b/Examples/MistDemo/Sources/MistDemoApp/Views/RecordDetailView.swift @@ -28,8 +28,8 @@ // #if canImport(SwiftUI) && canImport(CloudKit) - import MistDemoKit - import SwiftUI + internal import MistDemoKit + internal import SwiftUI /// Detail view showing all fields and metadata for a single Note record. internal struct RecordDetailView: View { diff --git a/Examples/MistDemo/Sources/MistDemoApp/Views/SidebarView.swift b/Examples/MistDemo/Sources/MistDemoApp/Views/SidebarView.swift index 4c4906ab..b2f4cd7f 100644 --- a/Examples/MistDemo/Sources/MistDemoApp/Views/SidebarView.swift +++ b/Examples/MistDemo/Sources/MistDemoApp/Views/SidebarView.swift @@ -28,7 +28,7 @@ // #if canImport(SwiftUI) - import SwiftUI + internal import SwiftUI /// Sidebar list of navigation items. internal struct SidebarView: View { diff --git a/Examples/MistDemo/Sources/MistDemoApp/Views/ZoneListView.swift b/Examples/MistDemo/Sources/MistDemoApp/Views/ZoneListView.swift index cb81d152..dd2686e1 100644 --- a/Examples/MistDemo/Sources/MistDemoApp/Views/ZoneListView.swift +++ b/Examples/MistDemo/Sources/MistDemoApp/Views/ZoneListView.swift @@ -28,8 +28,8 @@ // #if canImport(SwiftUI) && canImport(CloudKit) - import MistDemoKit - import SwiftUI + internal import MistDemoKit + internal import SwiftUI /// View listing all CloudKit record zones. internal struct ZoneListView: View { diff --git a/Examples/MistDemo/Sources/MistDemoKit/Commands/CreateCommand.swift b/Examples/MistDemo/Sources/MistDemoKit/Commands/CreateCommand.swift index cd31f8fd..425ebbff 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Commands/CreateCommand.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Commands/CreateCommand.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import MistKit +internal import Foundation +internal import MistKit /// Command to create a new record in CloudKit public struct CreateCommand: MistDemoCommand, OutputFormatting { diff --git a/Examples/MistDemo/Sources/MistDemoKit/Commands/CurrentUserCommand.swift b/Examples/MistDemo/Sources/MistDemoKit/Commands/CurrentUserCommand.swift index 6f02a7a4..145c5d15 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Commands/CurrentUserCommand.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Commands/CurrentUserCommand.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import MistKit +internal import Foundation +internal import MistKit /// Command to get information about the authenticated user public struct CurrentUserCommand: MistDemoCommand, OutputFormatting { diff --git a/Examples/MistDemo/Sources/MistDemoKit/Commands/DeleteCommand.swift b/Examples/MistDemo/Sources/MistDemoKit/Commands/DeleteCommand.swift index 94bf05f3..4bcbc8b8 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Commands/DeleteCommand.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Commands/DeleteCommand.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import MistKit +internal import Foundation +internal import MistKit /// Command to delete an existing record from CloudKit public struct DeleteCommand: MistDemoCommand, OutputFormatting { diff --git a/Examples/MistDemo/Sources/MistDemoKit/Commands/DeleteResult.swift b/Examples/MistDemo/Sources/MistDemoKit/Commands/DeleteResult.swift index 6869050b..0191f401 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Commands/DeleteResult.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Commands/DeleteResult.swift @@ -27,7 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation +internal import Foundation /// Result of a successful delete, formatted as command output. public struct DeleteResult: Encodable, Sendable { diff --git a/Examples/MistDemo/Sources/MistDemoKit/Commands/DemoErrorsCommand.swift b/Examples/MistDemo/Sources/MistDemoKit/Commands/DemoErrorsCommand.swift index 76cf3d35..8ab93bd7 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Commands/DemoErrorsCommand.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Commands/DemoErrorsCommand.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import MistKit +internal import Foundation +internal import MistKit /// Walks the audience through CloudKit's typed errors for the talk's /// "CloudKit as Your Backend" / Act 3, Step 4 — Error handling segment. diff --git a/Examples/MistDemo/Sources/MistDemoKit/Commands/DemoErrorsRunner+Output.swift b/Examples/MistDemo/Sources/MistDemoKit/Commands/DemoErrorsRunner+Output.swift index ad28ddc3..003c0348 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Commands/DemoErrorsRunner+Output.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Commands/DemoErrorsRunner+Output.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import MistKit +internal import Foundation +internal import MistKit extension DemoErrorsRunner { internal func printRunnerHeader() { diff --git a/Examples/MistDemo/Sources/MistDemoKit/Commands/DemoErrorsRunner.swift b/Examples/MistDemo/Sources/MistDemoKit/Commands/DemoErrorsRunner.swift index ce259489..ae9b8219 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Commands/DemoErrorsRunner.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Commands/DemoErrorsRunner.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import MistKit +internal import Foundation +internal import MistKit /// Runs the talk's CloudKit error scenarios (401, 404, 409) and prints typed /// `CloudKitError` details. Mirrors the section/prefix style of diff --git a/Examples/MistDemo/Sources/MistDemoKit/Commands/DemoInFilterCommand.swift b/Examples/MistDemo/Sources/MistDemoKit/Commands/DemoInFilterCommand.swift index b175b536..295fbea5 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Commands/DemoInFilterCommand.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Commands/DemoInFilterCommand.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import MistKit +internal import Foundation +internal import MistKit /// Demonstrates the IN/NOT_IN QueryFilter fix (issue #192) end-to-end. /// diff --git a/Examples/MistDemo/Sources/MistDemoKit/Commands/FetchChangesCommand.swift b/Examples/MistDemo/Sources/MistDemoKit/Commands/FetchChangesCommand.swift index 5f50cdeb..f22eefbb 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Commands/FetchChangesCommand.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Commands/FetchChangesCommand.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import MistKit +internal import Foundation +internal import MistKit /// Command to fetch record changes with incremental sync public struct FetchChangesCommand: MistDemoCommand, OutputFormatting { diff --git a/Examples/MistDemo/Sources/MistDemoKit/Commands/LookupCommand.swift b/Examples/MistDemo/Sources/MistDemoKit/Commands/LookupCommand.swift index 6871ebf5..3e70983f 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Commands/LookupCommand.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Commands/LookupCommand.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import MistKit +internal import Foundation +internal import MistKit /// Command to look up records by name in CloudKit public struct LookupCommand: MistDemoCommand, OutputFormatting { diff --git a/Examples/MistDemo/Sources/MistDemoKit/Commands/LookupZonesCommand.swift b/Examples/MistDemo/Sources/MistDemoKit/Commands/LookupZonesCommand.swift index 3d38519e..9a9b566a 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Commands/LookupZonesCommand.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Commands/LookupZonesCommand.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import MistKit +internal import Foundation +internal import MistKit /// Command to look up specific CloudKit zones by name public struct LookupZonesCommand: MistDemoCommand, OutputFormatting { diff --git a/Examples/MistDemo/Sources/MistDemoKit/Commands/MistDemoCommand.swift b/Examples/MistDemo/Sources/MistDemoKit/Commands/MistDemoCommand.swift index 98e40c8b..effa5155 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Commands/MistDemoCommand.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Commands/MistDemoCommand.swift @@ -28,7 +28,7 @@ // public import ConfigKeyKit -import Foundation +internal import Foundation /// Typealias for MistDemo commands - now uses generic Command protocol public typealias MistDemoCommand = Command diff --git a/Examples/MistDemo/Sources/MistDemoKit/Commands/ModifyCommand.swift b/Examples/MistDemo/Sources/MistDemoKit/Commands/ModifyCommand.swift index 8d11c206..0f37e6d4 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Commands/ModifyCommand.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Commands/ModifyCommand.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import MistKit +internal import Foundation +internal import MistKit /// Command to perform batch create/update/delete operations. public struct ModifyCommand: MistDemoCommand, OutputFormatting { diff --git a/Examples/MistDemo/Sources/MistDemoKit/Commands/ModifyOutput.swift b/Examples/MistDemo/Sources/MistDemoKit/Commands/ModifyOutput.swift index 60285350..8948628d 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Commands/ModifyOutput.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Commands/ModifyOutput.swift @@ -27,7 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation +internal import Foundation /// JSON envelope for modify output. public struct ModifyOutput: Encodable, Sendable { diff --git a/Examples/MistDemo/Sources/MistDemoKit/Commands/ModifyResultRow.swift b/Examples/MistDemo/Sources/MistDemoKit/Commands/ModifyResultRow.swift index d4ea90b7..cbdf5150 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Commands/ModifyResultRow.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Commands/ModifyResultRow.swift @@ -27,7 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation +internal import Foundation /// One row in the modify command's output. public struct ModifyResultRow: Encodable, Sendable { diff --git a/Examples/MistDemo/Sources/MistDemoKit/Commands/QueryCommand+FilterParsing.swift b/Examples/MistDemo/Sources/MistDemoKit/Commands/QueryCommand+FilterParsing.swift index 507c1e61..c2e899fc 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Commands/QueryCommand+FilterParsing.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Commands/QueryCommand+FilterParsing.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import MistKit +internal import Foundation +internal import MistKit extension QueryCommand { /// Parse a single filter expression "field:operator:value" into a QueryFilter diff --git a/Examples/MistDemo/Sources/MistDemoKit/Commands/QueryCommand.swift b/Examples/MistDemo/Sources/MistDemoKit/Commands/QueryCommand.swift index d9353970..a0e7ebc7 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Commands/QueryCommand.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Commands/QueryCommand.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import MistKit +internal import Foundation +internal import MistKit /// Command to query Note records from CloudKit with filtering and sorting public struct QueryCommand: MistDemoCommand, OutputFormatting { diff --git a/Examples/MistDemo/Sources/MistDemoKit/Commands/TestPrivateCommand.swift b/Examples/MistDemo/Sources/MistDemoKit/Commands/TestPrivateCommand.swift index b3b701c9..a3ebafe1 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Commands/TestPrivateCommand.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Commands/TestPrivateCommand.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import MistKit +internal import Foundation +internal import MistKit /// Command to run comprehensive integration tests against the private database, /// covering all CloudKit API methods including user-identity endpoints. diff --git a/Examples/MistDemo/Sources/MistDemoKit/Commands/TestPublicCommand.swift b/Examples/MistDemo/Sources/MistDemoKit/Commands/TestPublicCommand.swift index 408114b8..08255966 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Commands/TestPublicCommand.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Commands/TestPublicCommand.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import MistKit +internal import Foundation +internal import MistKit /// Command to run comprehensive integration tests for all CloudKit operations public struct TestPublicCommand: MistDemoCommand { diff --git a/Examples/MistDemo/Sources/MistDemoKit/Commands/UpdateCommand.swift b/Examples/MistDemo/Sources/MistDemoKit/Commands/UpdateCommand.swift index eabce926..9fd3c493 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Commands/UpdateCommand.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Commands/UpdateCommand.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import MistKit +internal import Foundation +internal import MistKit /// Command to update an existing record in CloudKit public struct UpdateCommand: MistDemoCommand, OutputFormatting { diff --git a/Examples/MistDemo/Sources/MistDemoKit/Commands/UploadAssetCommand.swift b/Examples/MistDemo/Sources/MistDemoKit/Commands/UploadAssetCommand.swift index 84c0b683..50ad9d49 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Commands/UploadAssetCommand.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Commands/UploadAssetCommand.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import MistKit +internal import Foundation +internal import MistKit /// Command to upload binary assets to CloudKit public struct UploadAssetCommand: MistDemoCommand, OutputFormatting { diff --git a/Examples/MistDemo/Sources/MistDemoKit/Configuration/AuthTokenConfig.swift b/Examples/MistDemo/Sources/MistDemoKit/Configuration/AuthTokenConfig.swift index 856b03b7..632b20a9 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Configuration/AuthTokenConfig.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Configuration/AuthTokenConfig.swift @@ -28,7 +28,7 @@ // public import ConfigKeyKit -import Foundation +internal import Foundation public import MistKit /// Configuration for auth-token command. diff --git a/Examples/MistDemo/Sources/MistDemoKit/Configuration/BrowserFlagResolver.swift b/Examples/MistDemo/Sources/MistDemoKit/Configuration/BrowserFlagResolver.swift index 5c39f1d2..9a1bdb3c 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Configuration/BrowserFlagResolver.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Configuration/BrowserFlagResolver.swift @@ -27,7 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation +internal import Foundation /// Resolves the "should we open the browser on startup?" decision from /// the two mutually-exclusive CLI flags into a single boolean. diff --git a/Examples/MistDemo/Sources/MistDemoKit/Configuration/ConfigurationError.swift b/Examples/MistDemo/Sources/MistDemoKit/Configuration/ConfigurationError.swift index b9eb0e66..d1a7377e 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Configuration/ConfigurationError.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Configuration/ConfigurationError.swift @@ -27,7 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation +internal import Foundation /// Configuration errors. internal enum ConfigurationError: LocalizedError { diff --git a/Examples/MistDemo/Sources/MistDemoKit/Configuration/CreateConfig.swift b/Examples/MistDemo/Sources/MistDemoKit/Configuration/CreateConfig.swift index c7bb3880..32a8e27e 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Configuration/CreateConfig.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Configuration/CreateConfig.swift @@ -28,7 +28,7 @@ // public import ConfigKeyKit -import Foundation +internal import Foundation public import MistKit /// Configuration for create command. diff --git a/Examples/MistDemo/Sources/MistDemoKit/Configuration/CurrentUserConfig.swift b/Examples/MistDemo/Sources/MistDemoKit/Configuration/CurrentUserConfig.swift index 17f23d98..bc806007 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Configuration/CurrentUserConfig.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Configuration/CurrentUserConfig.swift @@ -28,7 +28,7 @@ // public import ConfigKeyKit -import Foundation +internal import Foundation public import MistKit /// Configuration for current-user command. diff --git a/Examples/MistDemo/Sources/MistDemoKit/Configuration/DeleteConfig.swift b/Examples/MistDemo/Sources/MistDemoKit/Configuration/DeleteConfig.swift index 80adf6f4..2a6ed4bf 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Configuration/DeleteConfig.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Configuration/DeleteConfig.swift @@ -28,7 +28,7 @@ // public import ConfigKeyKit -import Foundation +internal import Foundation /// Configuration for delete command. public struct DeleteConfig: Sendable, ConfigurationParseable { diff --git a/Examples/MistDemo/Sources/MistDemoKit/Configuration/DemoErrorsConfig.swift b/Examples/MistDemo/Sources/MistDemoKit/Configuration/DemoErrorsConfig.swift index f2187d4c..8084bb00 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Configuration/DemoErrorsConfig.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Configuration/DemoErrorsConfig.swift @@ -28,7 +28,7 @@ // public import ConfigKeyKit -import Foundation +internal import Foundation /// Configuration for demo-errors command. public struct DemoErrorsConfig: Sendable, ConfigurationParseable { diff --git a/Examples/MistDemo/Sources/MistDemoKit/Configuration/DemoErrorsError.swift b/Examples/MistDemo/Sources/MistDemoKit/Configuration/DemoErrorsError.swift index 10568921..2a9f5808 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Configuration/DemoErrorsError.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Configuration/DemoErrorsError.swift @@ -27,7 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation +internal import Foundation /// Errors specific to the demo-errors command's configuration parsing. internal enum DemoErrorsError: LocalizedError { diff --git a/Examples/MistDemo/Sources/MistDemoKit/Configuration/Field.swift b/Examples/MistDemo/Sources/MistDemoKit/Configuration/Field.swift index b94b8ec9..1df3bd22 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Configuration/Field.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Configuration/Field.swift @@ -27,7 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation +internal import Foundation /// Field definition for create operations. public struct Field: Sendable { diff --git a/Examples/MistDemo/Sources/MistDemoKit/Configuration/FieldType.swift b/Examples/MistDemo/Sources/MistDemoKit/Configuration/FieldType.swift index 2eafc824..213934cf 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Configuration/FieldType.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Configuration/FieldType.swift @@ -27,7 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation +internal import Foundation /// Supported field types for CloudKit records. public enum FieldType: String, CaseIterable, Sendable { diff --git a/Examples/MistDemo/Sources/MistDemoKit/Configuration/LookupConfig.swift b/Examples/MistDemo/Sources/MistDemoKit/Configuration/LookupConfig.swift index 9c51fbbc..f03cacae 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Configuration/LookupConfig.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Configuration/LookupConfig.swift @@ -28,7 +28,7 @@ // public import ConfigKeyKit -import Foundation +internal import Foundation /// Configuration for lookup command. public struct LookupConfig: Sendable, ConfigurationParseable { diff --git a/Examples/MistDemo/Sources/MistDemoKit/Configuration/LookupZonesConfig.swift b/Examples/MistDemo/Sources/MistDemoKit/Configuration/LookupZonesConfig.swift index c1542073..f85215ba 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Configuration/LookupZonesConfig.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Configuration/LookupZonesConfig.swift @@ -28,7 +28,7 @@ // public import ConfigKeyKit -import Foundation +internal import Foundation /// Configuration for lookup-zones command. public struct LookupZonesConfig: Sendable, ConfigurationParseable { diff --git a/Examples/MistDemo/Sources/MistDemoKit/Configuration/MistDemoConfig+Parsing.swift b/Examples/MistDemo/Sources/MistDemoKit/Configuration/MistDemoConfig+Parsing.swift index d88d4af5..a793b4c6 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Configuration/MistDemoConfig+Parsing.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Configuration/MistDemoConfig+Parsing.swift @@ -27,7 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import MistKit +internal import MistKit extension MistDemoConfig { internal struct CoreConfig { diff --git a/Examples/MistDemo/Sources/MistDemoKit/Configuration/MistDemoConfig.swift b/Examples/MistDemo/Sources/MistDemoKit/Configuration/MistDemoConfig.swift index 64fe379a..0f0f3704 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Configuration/MistDemoConfig.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Configuration/MistDemoConfig.swift @@ -28,8 +28,8 @@ // public import ConfigKeyKit -import Configuration -import Foundation +internal import Configuration +internal import Foundation public import MistKit /// Centralized configuration for MistDemo. diff --git a/Examples/MistDemo/Sources/MistDemoKit/Configuration/MistDemoConfiguration.swift b/Examples/MistDemo/Sources/MistDemoKit/Configuration/MistDemoConfiguration.swift index 536402fe..cccb8c15 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Configuration/MistDemoConfiguration.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Configuration/MistDemoConfiguration.swift @@ -27,9 +27,9 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Configuration -import Foundation -import SystemPackage +internal import Configuration +internal import Foundation +internal import SystemPackage /// Swift Configuration-based setup for MistDemo. public struct MistDemoConfiguration: Sendable { diff --git a/Examples/MistDemo/Sources/MistDemoKit/Configuration/QueryConfig+Parsing.swift b/Examples/MistDemo/Sources/MistDemoKit/Configuration/QueryConfig+Parsing.swift index 1e49f23b..6d51e3c9 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Configuration/QueryConfig+Parsing.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Configuration/QueryConfig+Parsing.swift @@ -27,7 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation +internal import Foundation extension QueryConfig { internal struct ParsedPagination { diff --git a/Examples/MistDemo/Sources/MistDemoKit/Configuration/QueryConfig.swift b/Examples/MistDemo/Sources/MistDemoKit/Configuration/QueryConfig.swift index 813dd081..f5873971 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Configuration/QueryConfig.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Configuration/QueryConfig.swift @@ -28,7 +28,7 @@ // public import ConfigKeyKit -import Foundation +internal import Foundation public import MistKit /// Configuration for query command. diff --git a/Examples/MistDemo/Sources/MistDemoKit/Configuration/TestPrivateConfig.swift b/Examples/MistDemo/Sources/MistDemoKit/Configuration/TestPrivateConfig.swift index d4a70ba6..9b03c3c2 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Configuration/TestPrivateConfig.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Configuration/TestPrivateConfig.swift @@ -28,7 +28,7 @@ // public import ConfigKeyKit -import MistKit +internal import MistKit /// Configuration for test-private command (private database). public struct TestPrivateConfig: Sendable, ConfigurationParseable { diff --git a/Examples/MistDemo/Sources/MistDemoKit/Configuration/UpdateConfig.swift b/Examples/MistDemo/Sources/MistDemoKit/Configuration/UpdateConfig.swift index 6597bdcf..214576e3 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Configuration/UpdateConfig.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Configuration/UpdateConfig.swift @@ -28,7 +28,7 @@ // public import ConfigKeyKit -import Foundation +internal import Foundation public import MistKit /// Configuration for update command. diff --git a/Examples/MistDemo/Sources/MistDemoKit/Configuration/UploadAssetConfig.swift b/Examples/MistDemo/Sources/MistDemoKit/Configuration/UploadAssetConfig.swift index 4b38d8c5..71f6366a 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Configuration/UploadAssetConfig.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Configuration/UploadAssetConfig.swift @@ -28,7 +28,7 @@ // public import ConfigKeyKit -import Foundation +internal import Foundation public import MistKit /// Configuration for upload-asset command. diff --git a/Examples/MistDemo/Sources/MistDemoKit/Configuration/WebConfig.swift b/Examples/MistDemo/Sources/MistDemoKit/Configuration/WebConfig.swift index 8103853e..ebfd8557 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Configuration/WebConfig.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Configuration/WebConfig.swift @@ -28,7 +28,7 @@ // public import ConfigKeyKit -import Foundation +internal import Foundation public import MistKit /// Configuration for the long-running `web` demo command. diff --git a/Examples/MistDemo/Sources/MistDemoKit/Constants/MistDemoConstants+Defaults.swift b/Examples/MistDemo/Sources/MistDemoKit/Constants/MistDemoConstants+Defaults.swift index 27e100cc..afdb1044 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Constants/MistDemoConstants+Defaults.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Constants/MistDemoConstants+Defaults.swift @@ -27,7 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation +internal import Foundation extension MistDemoConstants { /// Default values for configuration parameters. diff --git a/Examples/MistDemo/Sources/MistDemoKit/Constants/MistDemoConstants+Messages.swift b/Examples/MistDemo/Sources/MistDemoKit/Constants/MistDemoConstants+Messages.swift index 19eb5331..ee986f40 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Constants/MistDemoConstants+Messages.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Constants/MistDemoConstants+Messages.swift @@ -27,7 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation +internal import Foundation extension MistDemoConstants { /// User-facing messages. diff --git a/Examples/MistDemo/Sources/MistDemoKit/Constants/MistDemoConstants.swift b/Examples/MistDemo/Sources/MistDemoKit/Constants/MistDemoConstants.swift index d502dc34..454c0aff 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Constants/MistDemoConstants.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Constants/MistDemoConstants.swift @@ -27,7 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation +internal import Foundation /// Central constants for MistDemo application. public enum MistDemoConstants { diff --git a/Examples/MistDemo/Sources/MistDemoKit/Errors/ErrorOutput+Convenience.swift b/Examples/MistDemo/Sources/MistDemoKit/Errors/ErrorOutput+Convenience.swift index acadabe8..c24aa758 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Errors/ErrorOutput+Convenience.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Errors/ErrorOutput+Convenience.swift @@ -27,7 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation +internal import Foundation // MARK: - JSON Encoding diff --git a/Examples/MistDemo/Sources/MistDemoKit/Errors/ErrorOutput.swift b/Examples/MistDemo/Sources/MistDemoKit/Errors/ErrorOutput.swift index f40e0be5..bd2d5832 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Errors/ErrorOutput.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Errors/ErrorOutput.swift @@ -27,7 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation +internal import Foundation /// JSON-formatted error output for consistent error reporting. public struct ErrorOutput: Sendable, Codable { diff --git a/Examples/MistDemo/Sources/MistDemoKit/Errors/FieldConversionError.swift b/Examples/MistDemo/Sources/MistDemoKit/Errors/FieldConversionError.swift index b9e26adc..2fd7c3bb 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Errors/FieldConversionError.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Errors/FieldConversionError.swift @@ -28,7 +28,7 @@ // public import Foundation -import MistKit +internal import MistKit /// Errors that can occur during field conversion public enum FieldConversionError: Error, LocalizedError { diff --git a/Examples/MistDemo/Sources/MistDemoKit/Errors/MistDemoError.swift b/Examples/MistDemo/Sources/MistDemoKit/Errors/MistDemoError.swift index 268d77fa..a31b47ba 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Errors/MistDemoError.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Errors/MistDemoError.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import MistKit +internal import Foundation +internal import MistKit /// Comprehensive error type for MistDemo operations. internal enum MistDemoError: LocalizedError, Sendable { diff --git a/Examples/MistDemo/Sources/MistDemoKit/Extensions/Array+Field.swift b/Examples/MistDemo/Sources/MistDemoKit/Extensions/Array+Field.swift index d82556fd..a80f5993 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Extensions/Array+Field.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Extensions/Array+Field.swift @@ -27,7 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation +internal import Foundation public import MistKit extension Array where Element == Field { diff --git a/Examples/MistDemo/Sources/MistDemoKit/Extensions/Command+AnyCommand.swift b/Examples/MistDemo/Sources/MistDemoKit/Extensions/Command+AnyCommand.swift index e422d33a..b2407e87 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Extensions/Command+AnyCommand.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Extensions/Command+AnyCommand.swift @@ -28,7 +28,7 @@ // public import ConfigKeyKit -import Foundation +internal import Foundation /// Default implementation of createInstance for all MistDemo commands. extension Command where Config.ConfigReader == MistDemoConfiguration { diff --git a/Examples/MistDemo/Sources/MistDemoKit/Extensions/ConfigKey+MistDemo.swift b/Examples/MistDemo/Sources/MistDemoKit/Extensions/ConfigKey+MistDemo.swift index a6a9ee7d..5f93d250 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Extensions/ConfigKey+MistDemo.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Extensions/ConfigKey+MistDemo.swift @@ -28,7 +28,7 @@ // public import ConfigKeyKit -import Foundation +internal import Foundation // MARK: - MistDemo-Specific Config Key Helpers diff --git a/Examples/MistDemo/Sources/MistDemoKit/Extensions/FieldValue+FieldType.swift b/Examples/MistDemo/Sources/MistDemoKit/Extensions/FieldValue+FieldType.swift index 6bf6a3ce..ccb5188e 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Extensions/FieldValue+FieldType.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Extensions/FieldValue+FieldType.swift @@ -27,7 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation +internal import Foundation public import MistKit extension FieldValue { diff --git a/Examples/MistDemo/Sources/MistDemoKit/Extensions/String+Padding.swift b/Examples/MistDemo/Sources/MistDemoKit/Extensions/String+Padding.swift index 9da66b3f..1d4b85ef 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Extensions/String+Padding.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Extensions/String+Padding.swift @@ -27,7 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation +internal import Foundation extension String { /// Pad the string on the left to the given width. diff --git a/Examples/MistDemo/Sources/MistDemoKit/Integration/AssetUploadReceipt+PhaseState.swift b/Examples/MistDemo/Sources/MistDemoKit/Integration/AssetUploadReceipt+PhaseState.swift index a994e9c8..94d569ce 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Integration/AssetUploadReceipt+PhaseState.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Integration/AssetUploadReceipt+PhaseState.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import MistKit +internal import Foundation +internal import MistKit extension AssetUploadReceipt: PhaseStateDecodable, PhaseStateEncodable { internal init(from state: PhaseState) throws { diff --git a/Examples/MistDemo/Sources/MistDemoKit/Integration/CleanupPhaseMarker.swift b/Examples/MistDemo/Sources/MistDemoKit/Integration/CleanupPhaseMarker.swift index 285e6ef1..2b2f260f 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Integration/CleanupPhaseMarker.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Integration/CleanupPhaseMarker.swift @@ -27,7 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation +internal import Foundation /// Marker protocol identifying the cleanup phase so the runner can skip it /// when `--skip-cleanup` is set and re-run it on failure. diff --git a/Examples/MistDemo/Sources/MistDemoKit/Integration/CreatedRecordNames.swift b/Examples/MistDemo/Sources/MistDemoKit/Integration/CreatedRecordNames.swift index f7508863..1bc7e979 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Integration/CreatedRecordNames.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Integration/CreatedRecordNames.swift @@ -27,7 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation +internal import Foundation /// Wraps the `createdRecordNames` slot of `PhaseState`. internal struct CreatedRecordNames: PhaseStateDecodable, diff --git a/Examples/MistDemo/Sources/MistDemoKit/Integration/IncrementalSyncInput.swift b/Examples/MistDemo/Sources/MistDemoKit/Integration/IncrementalSyncInput.swift index 468b3841..c47b2a27 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Integration/IncrementalSyncInput.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Integration/IncrementalSyncInput.swift @@ -27,7 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation +internal import Foundation /// Composite input read by `IncrementalSyncPhase`. internal struct IncrementalSyncInput: PhaseStateDecodable, Sendable { diff --git a/Examples/MistDemo/Sources/MistDemoKit/Integration/IntegrationPhase.swift b/Examples/MistDemo/Sources/MistDemoKit/Integration/IntegrationPhase.swift index 9194b8b6..cde5eb72 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Integration/IntegrationPhase.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Integration/IntegrationPhase.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import MistKit +internal import Foundation +internal import MistKit /// A single step in an integration test. /// diff --git a/Examples/MistDemo/Sources/MistDemoKit/Integration/IntegrationTest.swift b/Examples/MistDemo/Sources/MistDemoKit/Integration/IntegrationTest.swift index 40e8012c..d0472fe2 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Integration/IntegrationTest.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Integration/IntegrationTest.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import MistKit +internal import Foundation +internal import MistKit /// An integration test scenario -- typically one per CloudKit database. internal protocol IntegrationTest { diff --git a/Examples/MistDemo/Sources/MistDemoKit/Integration/IntegrationTestData.swift b/Examples/MistDemo/Sources/MistDemoKit/Integration/IntegrationTestData.swift index d5abeb00..a4a86c16 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Integration/IntegrationTestData.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Integration/IntegrationTestData.swift @@ -27,7 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation +internal import Foundation /// Test data generation utilities for integration tests. internal enum IntegrationTestData { diff --git a/Examples/MistDemo/Sources/MistDemoKit/Integration/IntegrationTestError.swift b/Examples/MistDemo/Sources/MistDemoKit/Integration/IntegrationTestError.swift index 75b13aaa..527baeff 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Integration/IntegrationTestError.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Integration/IntegrationTestError.swift @@ -27,7 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation +internal import Foundation /// Errors that can occur during integration testing. internal enum IntegrationTestError: LocalizedError, Sendable { diff --git a/Examples/MistDemo/Sources/MistDemoKit/Integration/IntegrationTestRunner.swift b/Examples/MistDemo/Sources/MistDemoKit/Integration/IntegrationTestRunner.swift index 0a23d6d6..f0a60bbc 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Integration/IntegrationTestRunner.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Integration/IntegrationTestRunner.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import MistKit +internal import Foundation +internal import MistKit /// Thin façade that builds a `PhaseContext` from CLI configuration and /// dispatches to the appropriate `PhasedIntegrationTest` implementation. diff --git a/Examples/MistDemo/Sources/MistDemoKit/Integration/NoState.swift b/Examples/MistDemo/Sources/MistDemoKit/Integration/NoState.swift index 84ee5a98..20071d5f 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Integration/NoState.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Integration/NoState.swift @@ -27,7 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation +internal import Foundation /// Sentinel used as `Input` or `Output` when a phase consumes or produces /// no `PhaseState`. Stands in for `Void`, which cannot conform to protocols. diff --git a/Examples/MistDemo/Sources/MistDemoKit/Integration/PhaseContext.swift b/Examples/MistDemo/Sources/MistDemoKit/Integration/PhaseContext.swift index c5a8148c..7bf738f3 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Integration/PhaseContext.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Integration/PhaseContext.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import MistKit +internal import Foundation +internal import MistKit /// Shared dependencies and configuration available to every phase. internal struct PhaseContext: Sendable { diff --git a/Examples/MistDemo/Sources/MistDemoKit/Integration/PhaseState.swift b/Examples/MistDemo/Sources/MistDemoKit/Integration/PhaseState.swift index 506c1594..e674fa62 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Integration/PhaseState.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Integration/PhaseState.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import MistKit +internal import Foundation +internal import MistKit /// Mutable state that flows between phases as the test progresses. /// diff --git a/Examples/MistDemo/Sources/MistDemoKit/Integration/PhaseStateDecodable.swift b/Examples/MistDemo/Sources/MistDemoKit/Integration/PhaseStateDecodable.swift index cba96cbe..5ecd0297 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Integration/PhaseStateDecodable.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Integration/PhaseStateDecodable.swift @@ -27,7 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation +internal import Foundation /// A type that can be initialized from `PhaseState`. /// diff --git a/Examples/MistDemo/Sources/MistDemoKit/Integration/PhaseStateEncodable.swift b/Examples/MistDemo/Sources/MistDemoKit/Integration/PhaseStateEncodable.swift index eb97a2e4..d5d41642 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Integration/PhaseStateEncodable.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Integration/PhaseStateEncodable.swift @@ -27,7 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation +internal import Foundation /// A type that can write itself into `PhaseState`. /// diff --git a/Examples/MistDemo/Sources/MistDemoKit/Integration/PhasedIntegrationTest.swift b/Examples/MistDemo/Sources/MistDemoKit/Integration/PhasedIntegrationTest.swift index 3e483c75..640df706 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Integration/PhasedIntegrationTest.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Integration/PhasedIntegrationTest.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import MistKit +internal import Foundation +internal import MistKit /// An integration test composed of an ordered list of phases. /// diff --git a/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/CleanupPhase.swift b/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/CleanupPhase.swift index 6016b9cc..ebd1d80a 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/CleanupPhase.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/CleanupPhase.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import MistKit +internal import Foundation +internal import MistKit internal struct CleanupPhase: IntegrationPhase, CleanupPhaseMarker { internal typealias Input = CreatedRecordNames diff --git a/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/CreateRecordsPhase.swift b/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/CreateRecordsPhase.swift index ef527616..20050934 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/CreateRecordsPhase.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/CreateRecordsPhase.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import MistKit +internal import Foundation +internal import MistKit internal struct CreateRecordsPhase: IntegrationPhase { internal typealias Input = AssetUploadReceipt diff --git a/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/DiscoverUserIdentitiesPhase.swift b/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/DiscoverUserIdentitiesPhase.swift index f6fb5b4e..ac7e3ae1 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/DiscoverUserIdentitiesPhase.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/DiscoverUserIdentitiesPhase.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import MistKit +internal import Foundation +internal import MistKit /// Calls POST `/users/discover` to look up specific user identities. /// diff --git a/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/FetchCallerPhase.swift b/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/FetchCallerPhase.swift index 327984e6..393e8829 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/FetchCallerPhase.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/FetchCallerPhase.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import MistKit +internal import Foundation +internal import MistKit /// Calls `users/caller`, the user-context endpoint that replaced the deprecated /// `users/current`. diff --git a/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/FetchZoneChangesPhase.swift b/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/FetchZoneChangesPhase.swift index f0c96345..bd536a02 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/FetchZoneChangesPhase.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/FetchZoneChangesPhase.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import MistKit +internal import Foundation +internal import MistKit internal struct FetchZoneChangesPhase: IntegrationPhase { internal typealias Input = NoState diff --git a/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/FinalVerificationPhase.swift b/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/FinalVerificationPhase.swift index 639f9a93..d97afe2b 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/FinalVerificationPhase.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/FinalVerificationPhase.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import MistKit +internal import Foundation +internal import MistKit internal struct FinalVerificationPhase: IntegrationPhase { internal typealias Input = NoState diff --git a/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/IncrementalSyncPhase.swift b/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/IncrementalSyncPhase.swift index 4fc3ae3b..a7c3fda4 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/IncrementalSyncPhase.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/IncrementalSyncPhase.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import MistKit +internal import Foundation +internal import MistKit internal struct IncrementalSyncPhase: IntegrationPhase { internal typealias Input = IncrementalSyncInput diff --git a/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/InitialSyncPhase.swift b/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/InitialSyncPhase.swift index 3a8ef274..86753e92 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/InitialSyncPhase.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/InitialSyncPhase.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import MistKit +internal import Foundation +internal import MistKit internal struct InitialSyncPhase: IntegrationPhase { internal typealias Input = CreatedRecordNames diff --git a/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/ListZonesPhase.swift b/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/ListZonesPhase.swift index 4f6881a8..1ca143cb 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/ListZonesPhase.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/ListZonesPhase.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import MistKit +internal import Foundation +internal import MistKit internal struct ListZonesPhase: IntegrationPhase { internal typealias Input = NoState diff --git a/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/LookupRecordsPhase.swift b/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/LookupRecordsPhase.swift index 6f91ac79..803ef055 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/LookupRecordsPhase.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/LookupRecordsPhase.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import MistKit +internal import Foundation +internal import MistKit internal struct LookupRecordsPhase: IntegrationPhase { internal typealias Input = CreatedRecordNames diff --git a/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/LookupUsersByEmailPhase.swift b/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/LookupUsersByEmailPhase.swift index 3dcca32e..326ed718 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/LookupUsersByEmailPhase.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/LookupUsersByEmailPhase.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import MistKit +internal import Foundation +internal import MistKit /// Calls POST `/users/lookup/email`. /// diff --git a/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/LookupUsersByRecordNamePhase.swift b/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/LookupUsersByRecordNamePhase.swift index 3d3465c5..2e5a963f 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/LookupUsersByRecordNamePhase.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/LookupUsersByRecordNamePhase.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import MistKit +internal import Foundation +internal import MistKit /// Calls POST `/users/lookup/id` with the caller's own user record name to /// exercise the endpoint via a self-lookup. diff --git a/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/LookupZonePhase.swift b/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/LookupZonePhase.swift index 6ebbd1b9..30321bf0 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/LookupZonePhase.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/LookupZonePhase.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import MistKit +internal import Foundation +internal import MistKit internal struct LookupZonePhase: IntegrationPhase { internal typealias Input = NoState diff --git a/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/ModifyRecordsPhase.swift b/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/ModifyRecordsPhase.swift index a2b19d1e..11eeeff9 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/ModifyRecordsPhase.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/ModifyRecordsPhase.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import MistKit +internal import Foundation +internal import MistKit internal struct ModifyRecordsPhase: IntegrationPhase { internal typealias Input = CreatedRecordNames diff --git a/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/QueryRecordsPhase.swift b/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/QueryRecordsPhase.swift index 0a91557f..678dfa72 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/QueryRecordsPhase.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/QueryRecordsPhase.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import MistKit +internal import Foundation +internal import MistKit internal struct QueryRecordsPhase: IntegrationPhase { internal typealias Input = CreatedRecordNames diff --git a/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/UploadAssetPhase.swift b/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/UploadAssetPhase.swift index 999c9045..3f762ab4 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/UploadAssetPhase.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/UploadAssetPhase.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import MistKit +internal import Foundation +internal import MistKit internal struct UploadAssetPhase: IntegrationPhase { internal typealias Input = NoState diff --git a/Examples/MistDemo/Sources/MistDemoKit/Integration/SyncTokenSlot.swift b/Examples/MistDemo/Sources/MistDemoKit/Integration/SyncTokenSlot.swift index 49d4f745..4ab9a90a 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Integration/SyncTokenSlot.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Integration/SyncTokenSlot.swift @@ -27,7 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation +internal import Foundation /// Wraps the `syncToken` slot of `PhaseState`. internal struct SyncTokenSlot: PhaseStateDecodable, diff --git a/Examples/MistDemo/Sources/MistDemoKit/Integration/Tests/PrivateDatabaseTest.swift b/Examples/MistDemo/Sources/MistDemoKit/Integration/Tests/PrivateDatabaseTest.swift index 3fbaac55..39d27c23 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Integration/Tests/PrivateDatabaseTest.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Integration/Tests/PrivateDatabaseTest.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import MistKit +internal import Foundation +internal import MistKit internal struct PrivateDatabaseTest: PhasedIntegrationTest { internal let name = "Private Database" diff --git a/Examples/MistDemo/Sources/MistDemoKit/Integration/Tests/PublicDatabaseTest.swift b/Examples/MistDemo/Sources/MistDemoKit/Integration/Tests/PublicDatabaseTest.swift index e8cdceca..b98c0e03 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Integration/Tests/PublicDatabaseTest.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Integration/Tests/PublicDatabaseTest.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import MistKit +internal import Foundation +internal import MistKit internal struct PublicDatabaseTest: PhasedIntegrationTest { internal let name = "Public Database" diff --git a/Examples/MistDemo/Sources/MistDemoKit/Integration/UserInfo+PhaseState.swift b/Examples/MistDemo/Sources/MistDemoKit/Integration/UserInfo+PhaseState.swift index 3661f435..8e82b2d4 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Integration/UserInfo+PhaseState.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Integration/UserInfo+PhaseState.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import MistKit +internal import Foundation +internal import MistKit extension UserInfo: PhaseStateDecodable, PhaseStateEncodable { internal init(from state: PhaseState) throws { diff --git a/Examples/MistDemo/Sources/MistDemoKit/MistDemoRunner.swift b/Examples/MistDemo/Sources/MistDemoKit/MistDemoRunner.swift index 170f5b3c..170f2f96 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/MistDemoRunner.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/MistDemoRunner.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import ConfigKeyKit -import Foundation +internal import ConfigKeyKit +internal import Foundation /// Top-level driver for the `mistdemo` CLI. Registers all available commands, /// parses arguments, and dispatches to the matching command — the executable diff --git a/Examples/MistDemo/Sources/MistDemoKit/Models/AuthRequest.swift b/Examples/MistDemo/Sources/MistDemoKit/Models/AuthRequest.swift index 507d9b18..3037e093 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Models/AuthRequest.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Models/AuthRequest.swift @@ -27,7 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation +internal import Foundation /// Request model for authentication callback from CloudKit Web Services. /// diff --git a/Examples/MistDemo/Sources/MistDemoKit/Output/Escapers/CSVEscaper.swift b/Examples/MistDemo/Sources/MistDemoKit/Output/Escapers/CSVEscaper.swift index 5ce1de49..1a908fe6 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Output/Escapers/CSVEscaper.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Output/Escapers/CSVEscaper.swift @@ -27,7 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation +internal import Foundation /// CSV escaper conforming to RFC 4180 public struct CSVEscaper: OutputEscaper { diff --git a/Examples/MistDemo/Sources/MistDemoKit/Output/Escapers/JSONEscaper.swift b/Examples/MistDemo/Sources/MistDemoKit/Output/Escapers/JSONEscaper.swift index f84338ec..72e379b6 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Output/Escapers/JSONEscaper.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Output/Escapers/JSONEscaper.swift @@ -27,7 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation +internal import Foundation /// JSON escaper (usually handled by JSONEncoder, but useful for manual JSON building) public struct JSONEscaper: OutputEscaper { diff --git a/Examples/MistDemo/Sources/MistDemoKit/Output/Escapers/OutputEscaperFactory.swift b/Examples/MistDemo/Sources/MistDemoKit/Output/Escapers/OutputEscaperFactory.swift index 4c114ec9..9622e827 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Output/Escapers/OutputEscaperFactory.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Output/Escapers/OutputEscaperFactory.swift @@ -27,7 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation +internal import Foundation /// Factory for creating output escapers based on output format public enum OutputEscaperFactory { diff --git a/Examples/MistDemo/Sources/MistDemoKit/Output/Escapers/TableEscaper.swift b/Examples/MistDemo/Sources/MistDemoKit/Output/Escapers/TableEscaper.swift index 2e354f66..f6dc026b 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Output/Escapers/TableEscaper.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Output/Escapers/TableEscaper.swift @@ -27,7 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation +internal import Foundation /// Table escaper for plain text table output public struct TableEscaper: OutputEscaper { diff --git a/Examples/MistDemo/Sources/MistDemoKit/Output/Escapers/YAMLEscaper.swift b/Examples/MistDemo/Sources/MistDemoKit/Output/Escapers/YAMLEscaper.swift index 73420ae8..1f5355a0 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Output/Escapers/YAMLEscaper.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Output/Escapers/YAMLEscaper.swift @@ -27,7 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation +internal import Foundation /// YAML escaper for proper string formatting public struct YAMLEscaper: OutputEscaper { diff --git a/Examples/MistDemo/Sources/MistDemoKit/Output/Formatters/CSVFormatter.swift b/Examples/MistDemo/Sources/MistDemoKit/Output/Formatters/CSVFormatter.swift index 519b900f..a56aa8a6 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Output/Formatters/CSVFormatter.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Output/Formatters/CSVFormatter.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import MistKit +internal import Foundation +internal import MistKit /// Formatter for CSV output public struct CSVFormatter: OutputFormatter { diff --git a/Examples/MistDemo/Sources/MistDemoKit/Output/Formatters/OutputFormatterFactory.swift b/Examples/MistDemo/Sources/MistDemoKit/Output/Formatters/OutputFormatterFactory.swift index 185b3d3a..0124588d 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Output/Formatters/OutputFormatterFactory.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Output/Formatters/OutputFormatterFactory.swift @@ -27,7 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation +internal import Foundation /// Factory for creating output formatters based on output format public enum OutputFormatterFactory { diff --git a/Examples/MistDemo/Sources/MistDemoKit/Output/Formatters/TableFormatter.swift b/Examples/MistDemo/Sources/MistDemoKit/Output/Formatters/TableFormatter.swift index 19a4efe4..de6b25cc 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Output/Formatters/TableFormatter.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Output/Formatters/TableFormatter.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import MistKit +internal import Foundation +internal import MistKit /// Formatter for table output public struct TableFormatter: OutputFormatter { diff --git a/Examples/MistDemo/Sources/MistDemoKit/Output/Formatters/YAMLFormatter.swift b/Examples/MistDemo/Sources/MistDemoKit/Output/Formatters/YAMLFormatter.swift index b7669305..485d1703 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Output/Formatters/YAMLFormatter.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Output/Formatters/YAMLFormatter.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import MistKit +internal import Foundation +internal import MistKit /// Formatter for YAML output public struct YAMLFormatter: OutputFormatter { diff --git a/Examples/MistDemo/Sources/MistDemoKit/Output/JSONFormatter.swift b/Examples/MistDemo/Sources/MistDemoKit/Output/JSONFormatter.swift index 34df776c..df9d83bd 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Output/JSONFormatter.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Output/JSONFormatter.swift @@ -27,7 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation +internal import Foundation /// Formatter for JSON output public struct JSONFormatter: OutputFormatter { diff --git a/Examples/MistDemo/Sources/MistDemoKit/Output/OutputFormat.swift b/Examples/MistDemo/Sources/MistDemoKit/Output/OutputFormat.swift index d719c329..8a6518cb 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Output/OutputFormat.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Output/OutputFormat.swift @@ -27,7 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation +internal import Foundation /// Supported output formats public enum OutputFormat: String, Sendable, CaseIterable { diff --git a/Examples/MistDemo/Sources/MistDemoKit/Output/OutputFormatter.swift b/Examples/MistDemo/Sources/MistDemoKit/Output/OutputFormatter.swift index 985051ee..4647038a 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Output/OutputFormatter.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Output/OutputFormatter.swift @@ -27,7 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation +internal import Foundation /// Protocol for formatting output in different formats public protocol OutputFormatter: Sendable { diff --git a/Examples/MistDemo/Sources/MistDemoKit/Output/Protocols/OutputEscaper.swift b/Examples/MistDemo/Sources/MistDemoKit/Output/Protocols/OutputEscaper.swift index d70cfcb2..5e897db3 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Output/Protocols/OutputEscaper.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Output/Protocols/OutputEscaper.swift @@ -27,7 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation +internal import Foundation /// Protocol for escaping strings for specific output formats public protocol OutputEscaper: Sendable { diff --git a/Examples/MistDemo/Sources/MistDemoKit/Protocols/OutputFormatting+Implementations.swift b/Examples/MistDemo/Sources/MistDemoKit/Protocols/OutputFormatting+Implementations.swift index 36a7c4af..074c2031 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Protocols/OutputFormatting+Implementations.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Protocols/OutputFormatting+Implementations.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import MistKit +internal import Foundation +internal import MistKit // MARK: - Format-specific implementations diff --git a/Examples/MistDemo/Sources/MistDemoKit/Protocols/OutputFormatting+Records.swift b/Examples/MistDemo/Sources/MistDemoKit/Protocols/OutputFormatting+Records.swift index 555cfd26..f8f4d53e 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Protocols/OutputFormatting+Records.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Protocols/OutputFormatting+Records.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import MistKit +internal import Foundation +internal import MistKit // MARK: - RecordInfo Output Formatting diff --git a/Examples/MistDemo/Sources/MistDemoKit/Protocols/OutputFormatting+Users.swift b/Examples/MistDemo/Sources/MistDemoKit/Protocols/OutputFormatting+Users.swift index 3cbe5ea7..031bdc7c 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Protocols/OutputFormatting+Users.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Protocols/OutputFormatting+Users.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import MistKit +internal import Foundation +internal import MistKit // MARK: - UserInfo Output Formatting diff --git a/Examples/MistDemo/Sources/MistDemoKit/Protocols/OutputFormatting.swift b/Examples/MistDemo/Sources/MistDemoKit/Protocols/OutputFormatting.swift index 0640a32b..b5f62a97 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Protocols/OutputFormatting.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Protocols/OutputFormatting.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import MistKit +internal import Foundation +internal import MistKit /// Protocol for formatting command output in different formats public protocol OutputFormatting { diff --git a/Examples/MistDemo/Sources/MistDemoKit/Types/AnyCodable.swift b/Examples/MistDemo/Sources/MistDemoKit/Types/AnyCodable.swift index 9698b522..8c3a7dc8 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Types/AnyCodable.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Types/AnyCodable.swift @@ -27,7 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation +internal import Foundation /// Helper for decoding arbitrary JSON values. internal struct AnyCodable: Codable { diff --git a/Examples/MistDemo/Sources/MistDemoKit/Types/DynamicKey.swift b/Examples/MistDemo/Sources/MistDemoKit/Types/DynamicKey.swift index 23f4e3b9..89b4398f 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Types/DynamicKey.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Types/DynamicKey.swift @@ -27,7 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation +internal import Foundation /// Dynamic coding key for handling arbitrary JSON object keys. internal struct DynamicKey: CodingKey { diff --git a/Examples/MistDemo/Sources/MistDemoKit/Types/FieldInputValue.swift b/Examples/MistDemo/Sources/MistDemoKit/Types/FieldInputValue.swift index 9d6125f0..945b9dfb 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Types/FieldInputValue.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Types/FieldInputValue.swift @@ -27,7 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation +internal import Foundation /// Enum representing different types of field input values public enum FieldInputValue: Sendable { diff --git a/Examples/MistDemo/Sources/MistDemoKit/Types/FieldsInput.swift b/Examples/MistDemo/Sources/MistDemoKit/Types/FieldsInput.swift index 276042c4..e810f977 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Types/FieldsInput.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Types/FieldsInput.swift @@ -27,7 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation +internal import Foundation /// Type-safe representation of field input from JSON public struct FieldsInput: Codable, Sendable { diff --git a/Examples/MistDemo/Sources/MistDemoKit/Utilities/AsyncHelpers.swift b/Examples/MistDemo/Sources/MistDemoKit/Utilities/AsyncHelpers.swift index a4845b16..ce043324 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Utilities/AsyncHelpers.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Utilities/AsyncHelpers.swift @@ -28,7 +28,7 @@ // public import Foundation -import UnixSignals +internal import UnixSignals /// Timeout error for async operations public enum AsyncTimeoutError: Error, LocalizedError { diff --git a/Examples/MistDemo/Sources/MistDemoKit/Utilities/AuthenticationError.swift b/Examples/MistDemo/Sources/MistDemoKit/Utilities/AuthenticationError.swift index fcd047b5..02fb3729 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Utilities/AuthenticationError.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Utilities/AuthenticationError.swift @@ -27,7 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation +internal import Foundation /// Errors that can occur during authentication setup. internal enum AuthenticationError: LocalizedError, Sendable { diff --git a/Examples/MistDemo/Sources/MistDemoKit/Utilities/AuthenticationHelper+SetupHelpers.swift b/Examples/MistDemo/Sources/MistDemoKit/Utilities/AuthenticationHelper+SetupHelpers.swift index 13d49e28..7e846105 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Utilities/AuthenticationHelper+SetupHelpers.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Utilities/AuthenticationHelper+SetupHelpers.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import MistKit +internal import Foundation +internal import MistKit extension AuthenticationHelper { internal static func setupServerToServer( diff --git a/Examples/MistDemo/Sources/MistDemoKit/Utilities/AuthenticationHelper.swift b/Examples/MistDemo/Sources/MistDemoKit/Utilities/AuthenticationHelper.swift index 0850ffb8..80e5c37f 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Utilities/AuthenticationHelper.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Utilities/AuthenticationHelper.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import MistKit +internal import Foundation +internal import MistKit /// Helper utilities for managing CloudKit authentication. internal enum AuthenticationHelper { diff --git a/Examples/MistDemo/Sources/MistDemoKit/Utilities/AuthenticationResult.swift b/Examples/MistDemo/Sources/MistDemoKit/Utilities/AuthenticationResult.swift index 30e1c689..0adc83a5 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Utilities/AuthenticationResult.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Utilities/AuthenticationResult.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import MistKit +internal import Foundation +internal import MistKit /// Result of authentication setup including token manager and selected database. internal struct AuthenticationResult { diff --git a/Examples/MistDemo/Sources/MistDemoKit/Utilities/BrowserOpener.swift b/Examples/MistDemo/Sources/MistDemoKit/Utilities/BrowserOpener.swift index 804aa561..b567c8a0 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Utilities/BrowserOpener.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Utilities/BrowserOpener.swift @@ -27,10 +27,10 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation +internal import Foundation #if canImport(AppKit) - import AppKit + internal import AppKit #endif /// Utility for opening URLs in the default browser. diff --git a/Examples/MistDemo/Sources/MistDemoKit/Utilities/FieldValueFormatter.swift b/Examples/MistDemo/Sources/MistDemoKit/Utilities/FieldValueFormatter.swift index 54099475..8d9d1645 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Utilities/FieldValueFormatter.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Utilities/FieldValueFormatter.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import MistKit +internal import Foundation +internal import MistKit /// Utility for formatting FieldValue objects for display. internal enum FieldValueFormatter { diff --git a/Examples/MistDemo/Tests/MistDemoTests/CloudKit/MistKitClientFactory/MistKitClientFactoryTests+APITokenOnly.swift b/Examples/MistDemo/Tests/MistDemoTests/CloudKit/MistKitClientFactory/MistKitClientFactoryTests+APITokenOnly.swift index 96465420..cfd36769 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/CloudKit/MistKitClientFactory/MistKitClientFactoryTests+APITokenOnly.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/CloudKit/MistKitClientFactory/MistKitClientFactoryTests+APITokenOnly.swift @@ -27,9 +27,9 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import MistKit -import Testing +internal import Foundation +internal import MistKit +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/CloudKit/MistKitClientFactory/MistKitClientFactoryTests+BadCredentials.swift b/Examples/MistDemo/Tests/MistDemoTests/CloudKit/MistKitClientFactory/MistKitClientFactoryTests+BadCredentials.swift index 1544f0cc..08bd2d0c 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/CloudKit/MistKitClientFactory/MistKitClientFactoryTests+BadCredentials.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/CloudKit/MistKitClientFactory/MistKitClientFactoryTests+BadCredentials.swift @@ -27,9 +27,9 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import MistKit -import Testing +internal import Foundation +internal import MistKit +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/CloudKit/MistKitClientFactory/MistKitClientFactoryTests+ContainerIdentifier.swift b/Examples/MistDemo/Tests/MistDemoTests/CloudKit/MistKitClientFactory/MistKitClientFactoryTests+ContainerIdentifier.swift index 02efd45a..2caf13b3 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/CloudKit/MistKitClientFactory/MistKitClientFactoryTests+ContainerIdentifier.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/CloudKit/MistKitClientFactory/MistKitClientFactoryTests+ContainerIdentifier.swift @@ -27,9 +27,9 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import MistKit -import Testing +internal import Foundation +internal import MistKit +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/CloudKit/MistKitClientFactory/MistKitClientFactoryTests+CustomTokenManager.swift b/Examples/MistDemo/Tests/MistDemoTests/CloudKit/MistKitClientFactory/MistKitClientFactoryTests+CustomTokenManager.swift index 1768ba67..201b4c86 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/CloudKit/MistKitClientFactory/MistKitClientFactoryTests+CustomTokenManager.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/CloudKit/MistKitClientFactory/MistKitClientFactoryTests+CustomTokenManager.swift @@ -27,9 +27,9 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import MistKit -import Testing +internal import Foundation +internal import MistKit +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/CloudKit/MistKitClientFactory/MistKitClientFactoryTests+Environment.swift b/Examples/MistDemo/Tests/MistDemoTests/CloudKit/MistKitClientFactory/MistKitClientFactoryTests+Environment.swift index 6f43444d..f9622a82 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/CloudKit/MistKitClientFactory/MistKitClientFactoryTests+Environment.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/CloudKit/MistKitClientFactory/MistKitClientFactoryTests+Environment.swift @@ -27,9 +27,9 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import MistKit -import Testing +internal import Foundation +internal import MistKit +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/CloudKit/MistKitClientFactory/MistKitClientFactoryTests+ErrorCases.swift b/Examples/MistDemo/Tests/MistDemoTests/CloudKit/MistKitClientFactory/MistKitClientFactoryTests+ErrorCases.swift index 27c5262e..cab0152e 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/CloudKit/MistKitClientFactory/MistKitClientFactoryTests+ErrorCases.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/CloudKit/MistKitClientFactory/MistKitClientFactoryTests+ErrorCases.swift @@ -27,9 +27,9 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import MistKit -import Testing +internal import Foundation +internal import MistKit +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/CloudKit/MistKitClientFactory/MistKitClientFactoryTests+Helpers.swift b/Examples/MistDemo/Tests/MistDemoTests/CloudKit/MistKitClientFactory/MistKitClientFactoryTests+Helpers.swift index 451cfb63..abd9ad27 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/CloudKit/MistKitClientFactory/MistKitClientFactoryTests+Helpers.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/CloudKit/MistKitClientFactory/MistKitClientFactoryTests+Helpers.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import MistKit +internal import Foundation +internal import MistKit @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/CloudKit/MistKitClientFactory/MistKitClientFactoryTests+PrivateKeyFile.swift b/Examples/MistDemo/Tests/MistDemoTests/CloudKit/MistKitClientFactory/MistKitClientFactoryTests+PrivateKeyFile.swift index 4be32f18..7b77c525 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/CloudKit/MistKitClientFactory/MistKitClientFactoryTests+PrivateKeyFile.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/CloudKit/MistKitClientFactory/MistKitClientFactoryTests+PrivateKeyFile.swift @@ -27,9 +27,9 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import MistKit -import Testing +internal import Foundation +internal import MistKit +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/CloudKit/MistKitClientFactory/MistKitClientFactoryTests+PublicDatabase.swift b/Examples/MistDemo/Tests/MistDemoTests/CloudKit/MistKitClientFactory/MistKitClientFactoryTests+PublicDatabase.swift index 867e38da..57b08236 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/CloudKit/MistKitClientFactory/MistKitClientFactoryTests+PublicDatabase.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/CloudKit/MistKitClientFactory/MistKitClientFactoryTests+PublicDatabase.swift @@ -27,9 +27,9 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import MistKit -import Testing +internal import Foundation +internal import MistKit +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/CloudKit/MistKitClientFactory/MistKitClientFactoryTests+ServerToServerAuth.swift b/Examples/MistDemo/Tests/MistDemoTests/CloudKit/MistKitClientFactory/MistKitClientFactoryTests+ServerToServerAuth.swift index 8b37fb3d..071a1b08 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/CloudKit/MistKitClientFactory/MistKitClientFactoryTests+ServerToServerAuth.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/CloudKit/MistKitClientFactory/MistKitClientFactoryTests+ServerToServerAuth.swift @@ -27,9 +27,9 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import MistKit -import Testing +internal import Foundation +internal import MistKit +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/CloudKit/MistKitClientFactory/MistKitClientFactoryTests+WebAuthToken.swift b/Examples/MistDemo/Tests/MistDemoTests/CloudKit/MistKitClientFactory/MistKitClientFactoryTests+WebAuthToken.swift index e4b1ed4a..1ed9eaf3 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/CloudKit/MistKitClientFactory/MistKitClientFactoryTests+WebAuthToken.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/CloudKit/MistKitClientFactory/MistKitClientFactoryTests+WebAuthToken.swift @@ -27,9 +27,9 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import MistKit -import Testing +internal import Foundation +internal import MistKit +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/CloudKit/MistKitClientFactory/MistKitClientFactoryTests.swift b/Examples/MistDemo/Tests/MistDemoTests/CloudKit/MistKitClientFactory/MistKitClientFactoryTests.swift index e128e53f..37659781 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/CloudKit/MistKitClientFactory/MistKitClientFactoryTests.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/CloudKit/MistKitClientFactory/MistKitClientFactoryTests.swift @@ -27,7 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Testing +internal import Testing @Suite( "MistKitClientFactory", diff --git a/Examples/MistDemo/Tests/MistDemoTests/Commands/AuthTokenCommand/AuthTokenCommandTests+AsyncChannel.swift b/Examples/MistDemo/Tests/MistDemoTests/Commands/AuthTokenCommand/AuthTokenCommandTests+AsyncChannel.swift index e6566b45..5b03bea4 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Commands/AuthTokenCommand/AuthTokenCommandTests+AsyncChannel.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Commands/AuthTokenCommand/AuthTokenCommandTests+AsyncChannel.swift @@ -28,9 +28,9 @@ // #if canImport(Hummingbird) - import AsyncAlgorithms - import Foundation - import Testing + internal import AsyncAlgorithms + internal import Foundation + internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Commands/AuthTokenCommand/AuthTokenCommandTests+CommandInitialization.swift b/Examples/MistDemo/Tests/MistDemoTests/Commands/AuthTokenCommand/AuthTokenCommandTests+CommandInitialization.swift index fc49d7eb..6ff858b6 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Commands/AuthTokenCommand/AuthTokenCommandTests+CommandInitialization.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Commands/AuthTokenCommand/AuthTokenCommandTests+CommandInitialization.swift @@ -28,8 +28,8 @@ // #if canImport(Hummingbird) - import Foundation - import Testing + internal import Foundation + internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Commands/AuthTokenCommand/AuthTokenCommandTests+Configuration.swift b/Examples/MistDemo/Tests/MistDemoTests/Commands/AuthTokenCommand/AuthTokenCommandTests+Configuration.swift index b07e39a5..515b2674 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Commands/AuthTokenCommand/AuthTokenCommandTests+Configuration.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Commands/AuthTokenCommand/AuthTokenCommandTests+Configuration.swift @@ -28,8 +28,8 @@ // #if canImport(Hummingbird) - import Foundation - import Testing + internal import Foundation + internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Commands/AuthTokenCommand/AuthTokenCommandTests+Error.swift b/Examples/MistDemo/Tests/MistDemoTests/Commands/AuthTokenCommand/AuthTokenCommandTests+Error.swift index f0b5396b..c6d2d179 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Commands/AuthTokenCommand/AuthTokenCommandTests+Error.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Commands/AuthTokenCommand/AuthTokenCommandTests+Error.swift @@ -28,8 +28,8 @@ // #if canImport(Hummingbird) - import Foundation - import Testing + internal import Foundation + internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Commands/AuthTokenCommand/AuthTokenCommandTests+MockServer.swift b/Examples/MistDemo/Tests/MistDemoTests/Commands/AuthTokenCommand/AuthTokenCommandTests+MockServer.swift index 4eff29f5..cd18aa94 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Commands/AuthTokenCommand/AuthTokenCommandTests+MockServer.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Commands/AuthTokenCommand/AuthTokenCommandTests+MockServer.swift @@ -28,8 +28,8 @@ // #if canImport(Hummingbird) - import Foundation - import Testing + internal import Foundation + internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Commands/AuthTokenCommand/AuthTokenCommandTests+Timeout.swift b/Examples/MistDemo/Tests/MistDemoTests/Commands/AuthTokenCommand/AuthTokenCommandTests+Timeout.swift index 3c9dc7b7..4b6fabe8 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Commands/AuthTokenCommand/AuthTokenCommandTests+Timeout.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Commands/AuthTokenCommand/AuthTokenCommandTests+Timeout.swift @@ -28,8 +28,8 @@ // #if canImport(Hummingbird) - import Foundation - import Testing + internal import Foundation + internal import Testing @testable import MistDemoKit @@ -44,11 +44,13 @@ ) ) internal func timeoutHelperThrowsOnTimeout() async { - // Mirrors AsyncHelpersTests+Timeout's gate: on simulator cooperative - // executors (notably visionOS / watchOS under CI load) the operation's - // single long Task.sleep can complete before the polling timeout - // task's many short sleeps detect the deadline. - await withKnownIssue(isIntermittent: true) { + // Mirrors AsyncHelpersTests+Timeout's gate: intermittent only on + // `TestPlatform.isFlakyTimeoutSimulator` (CI visionOS / watchOS sim), + // strict everywhere else. See #334. + await withKnownIssue( + isIntermittent: true, + when: TestPlatform.isFlakyTimeoutSimulator + ) { await #expect(throws: AsyncTimeoutError.self) { try await withTimeoutAndSignals(seconds: 0.1) { try await Task.sleep(nanoseconds: 1_000_000_000) diff --git a/Examples/MistDemo/Tests/MistDemoTests/Commands/AuthTokenCommand/AuthTokenCommandTests.swift b/Examples/MistDemo/Tests/MistDemoTests/Commands/AuthTokenCommand/AuthTokenCommandTests.swift index 81cb23b0..5e87cf43 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Commands/AuthTokenCommand/AuthTokenCommandTests.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Commands/AuthTokenCommand/AuthTokenCommandTests.swift @@ -28,7 +28,7 @@ // #if canImport(Hummingbird) - import Testing + internal import Testing @Suite("AuthTokenCommand") internal enum AuthTokenCommandTests {} diff --git a/Examples/MistDemo/Tests/MistDemoTests/Commands/CommandIntegration/CommandIntegrationTests+AuthTokenCommandIntegration.swift b/Examples/MistDemo/Tests/MistDemoTests/Commands/CommandIntegration/CommandIntegrationTests+AuthTokenCommandIntegration.swift index 899e769b..0c0ddd78 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Commands/CommandIntegration/CommandIntegrationTests+AuthTokenCommandIntegration.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Commands/CommandIntegration/CommandIntegrationTests+AuthTokenCommandIntegration.swift @@ -28,9 +28,9 @@ // #if canImport(Hummingbird) - import Foundation - import MistKit - import Testing + internal import Foundation + internal import MistKit + internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Commands/CommandIntegration/CommandIntegrationTests+CreateCommandIntegration.swift b/Examples/MistDemo/Tests/MistDemoTests/Commands/CommandIntegration/CommandIntegrationTests+CreateCommandIntegration.swift index 7cb9c9e9..04c57633 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Commands/CommandIntegration/CommandIntegrationTests+CreateCommandIntegration.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Commands/CommandIntegration/CommandIntegrationTests+CreateCommandIntegration.swift @@ -27,9 +27,9 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import MistKit -import Testing +internal import Foundation +internal import MistKit +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Commands/CommandIntegration/CommandIntegrationTests+CrossCommandIntegration.swift b/Examples/MistDemo/Tests/MistDemoTests/Commands/CommandIntegration/CommandIntegrationTests+CrossCommandIntegration.swift index 2684d1de..7742d093 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Commands/CommandIntegration/CommandIntegrationTests+CrossCommandIntegration.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Commands/CommandIntegration/CommandIntegrationTests+CrossCommandIntegration.swift @@ -27,9 +27,9 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import MistKit -import Testing +internal import Foundation +internal import MistKit +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Commands/CommandIntegration/CommandIntegrationTests+CurrentUserCommandIntegration.swift b/Examples/MistDemo/Tests/MistDemoTests/Commands/CommandIntegration/CommandIntegrationTests+CurrentUserCommandIntegration.swift index cb2fcecf..b16bea54 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Commands/CommandIntegration/CommandIntegrationTests+CurrentUserCommandIntegration.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Commands/CommandIntegration/CommandIntegrationTests+CurrentUserCommandIntegration.swift @@ -27,9 +27,9 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import MistKit -import Testing +internal import Foundation +internal import MistKit +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Commands/CommandIntegration/CommandIntegrationTests+ErrorHandlingIntegration.swift b/Examples/MistDemo/Tests/MistDemoTests/Commands/CommandIntegration/CommandIntegrationTests+ErrorHandlingIntegration.swift index 3fcb4ef7..72c6ba8a 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Commands/CommandIntegration/CommandIntegrationTests+ErrorHandlingIntegration.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Commands/CommandIntegration/CommandIntegrationTests+ErrorHandlingIntegration.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Commands/CommandIntegration/CommandIntegrationTests+QueryCommandIntegration.swift b/Examples/MistDemo/Tests/MistDemoTests/Commands/CommandIntegration/CommandIntegrationTests+QueryCommandIntegration.swift index 308df2e5..171012ca 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Commands/CommandIntegration/CommandIntegrationTests+QueryCommandIntegration.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Commands/CommandIntegration/CommandIntegrationTests+QueryCommandIntegration.swift @@ -27,9 +27,9 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import MistKit -import Testing +internal import Foundation +internal import MistKit +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Commands/CommandIntegration/CommandIntegrationTests+RealWorldUsageSimulation.swift b/Examples/MistDemo/Tests/MistDemoTests/Commands/CommandIntegration/CommandIntegrationTests+RealWorldUsageSimulation.swift index a9a1f579..6584faaa 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Commands/CommandIntegration/CommandIntegrationTests+RealWorldUsageSimulation.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Commands/CommandIntegration/CommandIntegrationTests+RealWorldUsageSimulation.swift @@ -27,9 +27,9 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import MistKit -import Testing +internal import Foundation +internal import MistKit +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Commands/CommandIntegration/CommandIntegrationTests.swift b/Examples/MistDemo/Tests/MistDemoTests/Commands/CommandIntegration/CommandIntegrationTests.swift index 80528183..b6714a6b 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Commands/CommandIntegration/CommandIntegrationTests.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Commands/CommandIntegration/CommandIntegrationTests.swift @@ -27,7 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Testing +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Commands/CreateCommand/CreateCommandTests+CommandProperty.swift b/Examples/MistDemo/Tests/MistDemoTests/Commands/CreateCommand/CreateCommandTests+CommandProperty.swift index c10376be..e250d744 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Commands/CreateCommand/CreateCommandTests+CommandProperty.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Commands/CreateCommand/CreateCommandTests+CommandProperty.swift @@ -27,9 +27,9 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import MistKit -import Testing +internal import Foundation +internal import MistKit +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Commands/CreateCommand/CreateCommandTests+Configuration.swift b/Examples/MistDemo/Tests/MistDemoTests/Commands/CreateCommand/CreateCommandTests+Configuration.swift index da897fd3..cc200948 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Commands/CreateCommand/CreateCommandTests+Configuration.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Commands/CreateCommand/CreateCommandTests+Configuration.swift @@ -27,9 +27,9 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import MistKit -import Testing +internal import Foundation +internal import MistKit +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Commands/CreateCommand/CreateCommandTests+ErrorHandling.swift b/Examples/MistDemo/Tests/MistDemoTests/Commands/CreateCommand/CreateCommandTests+ErrorHandling.swift index fedb237a..eccd1678 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Commands/CreateCommand/CreateCommandTests+ErrorHandling.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Commands/CreateCommand/CreateCommandTests+ErrorHandling.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Commands/CreateCommand/CreateCommandTests+FieldParsing.swift b/Examples/MistDemo/Tests/MistDemoTests/Commands/CreateCommand/CreateCommandTests+FieldParsing.swift index 343583cf..88a8e40d 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Commands/CreateCommand/CreateCommandTests+FieldParsing.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Commands/CreateCommand/CreateCommandTests+FieldParsing.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Commands/CreateCommand/CreateCommandTests+FieldType.swift b/Examples/MistDemo/Tests/MistDemoTests/Commands/CreateCommand/CreateCommandTests+FieldType.swift index 494d66ae..163a0e8d 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Commands/CreateCommand/CreateCommandTests+FieldType.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Commands/CreateCommand/CreateCommandTests+FieldType.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Commands/CreateCommand/CreateCommandTests+FieldTypeConversion.swift b/Examples/MistDemo/Tests/MistDemoTests/Commands/CreateCommand/CreateCommandTests+FieldTypeConversion.swift index 84a0f2b7..d3434af8 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Commands/CreateCommand/CreateCommandTests+FieldTypeConversion.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Commands/CreateCommand/CreateCommandTests+FieldTypeConversion.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Commands/CreateCommand/CreateCommandTests+FieldValidation.swift b/Examples/MistDemo/Tests/MistDemoTests/Commands/CreateCommand/CreateCommandTests+FieldValidation.swift index c23fb097..c4e6046f 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Commands/CreateCommand/CreateCommandTests+FieldValidation.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Commands/CreateCommand/CreateCommandTests+FieldValidation.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Commands/CreateCommand/CreateCommandTests+GenerateRecordName.swift b/Examples/MistDemo/Tests/MistDemoTests/Commands/CreateCommand/CreateCommandTests+GenerateRecordName.swift index 84db2408..ae36a231 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Commands/CreateCommand/CreateCommandTests+GenerateRecordName.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Commands/CreateCommand/CreateCommandTests+GenerateRecordName.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Commands/CreateCommand/CreateCommandTests+JSONFieldLoading.swift b/Examples/MistDemo/Tests/MistDemoTests/Commands/CreateCommand/CreateCommandTests+JSONFieldLoading.swift index 0b575a2e..f9b8a5c5 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Commands/CreateCommand/CreateCommandTests+JSONFieldLoading.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Commands/CreateCommand/CreateCommandTests+JSONFieldLoading.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Commands/CreateCommand/CreateCommandTests+MultipleFieldParsing.swift b/Examples/MistDemo/Tests/MistDemoTests/Commands/CreateCommand/CreateCommandTests+MultipleFieldParsing.swift index 0a0858e4..73bf3501 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Commands/CreateCommand/CreateCommandTests+MultipleFieldParsing.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Commands/CreateCommand/CreateCommandTests+MultipleFieldParsing.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Commands/CreateCommand/CreateCommandTests+RecordNameGeneration.swift b/Examples/MistDemo/Tests/MistDemoTests/Commands/CreateCommand/CreateCommandTests+RecordNameGeneration.swift index f441cf3f..b627b8b7 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Commands/CreateCommand/CreateCommandTests+RecordNameGeneration.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Commands/CreateCommand/CreateCommandTests+RecordNameGeneration.swift @@ -27,9 +27,9 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import MistKit -import Testing +internal import Foundation +internal import MistKit +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Commands/CreateCommand/CreateCommandTests.swift b/Examples/MistDemo/Tests/MistDemoTests/Commands/CreateCommand/CreateCommandTests.swift index 3ebf47ff..429432ae 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Commands/CreateCommand/CreateCommandTests.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Commands/CreateCommand/CreateCommandTests.swift @@ -27,7 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Testing +internal import Testing @Suite("CreateCommand") internal enum CreateCommandTests {} diff --git a/Examples/MistDemo/Tests/MistDemoTests/Commands/CurrentUserCommand/CurrentUserCommandTests+CommandProperty.swift b/Examples/MistDemo/Tests/MistDemoTests/Commands/CurrentUserCommand/CurrentUserCommandTests+CommandProperty.swift index 86862304..d8b03a45 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Commands/CurrentUserCommand/CurrentUserCommandTests+CommandProperty.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Commands/CurrentUserCommand/CurrentUserCommandTests+CommandProperty.swift @@ -27,9 +27,9 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import MistKit -import Testing +internal import Foundation +internal import MistKit +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Commands/CurrentUserCommand/CurrentUserCommandTests+Configuration.swift b/Examples/MistDemo/Tests/MistDemoTests/Commands/CurrentUserCommand/CurrentUserCommandTests+Configuration.swift index 0a9546bd..258c8ea8 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Commands/CurrentUserCommand/CurrentUserCommandTests+Configuration.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Commands/CurrentUserCommand/CurrentUserCommandTests+Configuration.swift @@ -27,9 +27,9 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import MistKit -import Testing +internal import Foundation +internal import MistKit +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Commands/CurrentUserCommand/CurrentUserCommandTests+DatabaseSelection.swift b/Examples/MistDemo/Tests/MistDemoTests/Commands/CurrentUserCommand/CurrentUserCommandTests+DatabaseSelection.swift index e94be396..90c895c4 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Commands/CurrentUserCommand/CurrentUserCommandTests+DatabaseSelection.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Commands/CurrentUserCommand/CurrentUserCommandTests+DatabaseSelection.swift @@ -27,9 +27,9 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import MistKit -import Testing +internal import Foundation +internal import MistKit +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Commands/CurrentUserCommand/CurrentUserCommandTests+ErrorHandling.swift b/Examples/MistDemo/Tests/MistDemoTests/Commands/CurrentUserCommand/CurrentUserCommandTests+ErrorHandling.swift index 9d4270ef..d2ae4e25 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Commands/CurrentUserCommand/CurrentUserCommandTests+ErrorHandling.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Commands/CurrentUserCommand/CurrentUserCommandTests+ErrorHandling.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Commands/CurrentUserCommand/CurrentUserCommandTests+FieldFiltering.swift b/Examples/MistDemo/Tests/MistDemoTests/Commands/CurrentUserCommand/CurrentUserCommandTests+FieldFiltering.swift index f4faa0cf..fadc7a38 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Commands/CurrentUserCommand/CurrentUserCommandTests+FieldFiltering.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Commands/CurrentUserCommand/CurrentUserCommandTests+FieldFiltering.swift @@ -27,9 +27,9 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import MistKit -import Testing +internal import Foundation +internal import MistKit +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Commands/CurrentUserCommand/CurrentUserCommandTests+MistKitClientFactoryIntegration.swift b/Examples/MistDemo/Tests/MistDemoTests/Commands/CurrentUserCommand/CurrentUserCommandTests+MistKitClientFactoryIntegration.swift index 4ec14d02..4d02811c 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Commands/CurrentUserCommand/CurrentUserCommandTests+MistKitClientFactoryIntegration.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Commands/CurrentUserCommand/CurrentUserCommandTests+MistKitClientFactoryIntegration.swift @@ -27,9 +27,9 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import MistKit -import Testing +internal import Foundation +internal import MistKit +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Commands/CurrentUserCommand/CurrentUserCommandTests+MockUserResponse.swift b/Examples/MistDemo/Tests/MistDemoTests/Commands/CurrentUserCommand/CurrentUserCommandTests+MockUserResponse.swift index f0374f62..3b42d191 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Commands/CurrentUserCommand/CurrentUserCommandTests+MockUserResponse.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Commands/CurrentUserCommand/CurrentUserCommandTests+MockUserResponse.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Commands/CurrentUserCommand/CurrentUserCommandTests+OutputFormat.swift b/Examples/MistDemo/Tests/MistDemoTests/Commands/CurrentUserCommand/CurrentUserCommandTests+OutputFormat.swift index 8cd67cd9..8e38c2d6 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Commands/CurrentUserCommand/CurrentUserCommandTests+OutputFormat.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Commands/CurrentUserCommand/CurrentUserCommandTests+OutputFormat.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Commands/CurrentUserCommand/CurrentUserCommandTests.swift b/Examples/MistDemo/Tests/MistDemoTests/Commands/CurrentUserCommand/CurrentUserCommandTests.swift index 200af07e..bfbb2a98 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Commands/CurrentUserCommand/CurrentUserCommandTests.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Commands/CurrentUserCommand/CurrentUserCommandTests.swift @@ -27,7 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Testing +internal import Testing @Suite("CurrentUserCommand") internal enum CurrentUserCommandTests {} diff --git a/Examples/MistDemo/Tests/MistDemoTests/Commands/DeleteCommandMapConflictTests.swift b/Examples/MistDemo/Tests/MistDemoTests/Commands/DeleteCommandMapConflictTests.swift index a783ca96..37ceb6df 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Commands/DeleteCommandMapConflictTests.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Commands/DeleteCommandMapConflictTests.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import MistKit -import Testing +internal import MistKit +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Commands/DeleteCommandTests.swift b/Examples/MistDemo/Tests/MistDemoTests/Commands/DeleteCommandTests.swift index 770dee1a..e105c10f 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Commands/DeleteCommandTests.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Commands/DeleteCommandTests.swift @@ -27,7 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Testing +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Commands/DemoErrorsRunnerOutputTests.swift b/Examples/MistDemo/Tests/MistDemoTests/Commands/DemoErrorsRunnerOutputTests.swift index fc4945ef..23f823f6 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Commands/DemoErrorsRunnerOutputTests.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Commands/DemoErrorsRunnerOutputTests.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Commands/LookupCommandTests.swift b/Examples/MistDemo/Tests/MistDemoTests/Commands/LookupCommandTests.swift index b758d588..4bf29244 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Commands/LookupCommandTests.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Commands/LookupCommandTests.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Commands/ModifyCommandTests.swift b/Examples/MistDemo/Tests/MistDemoTests/Commands/ModifyCommandTests.swift index e6d1f5d6..2ce2becb 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Commands/ModifyCommandTests.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Commands/ModifyCommandTests.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Commands/ModifyOutputTests.swift b/Examples/MistDemo/Tests/MistDemoTests/Commands/ModifyOutputTests.swift index 633a2fe8..78c6558d 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Commands/ModifyOutputTests.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Commands/ModifyOutputTests.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Commands/ModifyResultRowTests.swift b/Examples/MistDemo/Tests/MistDemoTests/Commands/ModifyResultRowTests.swift index 3931642f..26e7d84b 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Commands/ModifyResultRowTests.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Commands/ModifyResultRowTests.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Commands/QueryCommand/QueryCommandTests+CommandProperty.swift b/Examples/MistDemo/Tests/MistDemoTests/Commands/QueryCommand/QueryCommandTests+CommandProperty.swift index 48b4788c..b1a9d4ea 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Commands/QueryCommand/QueryCommandTests+CommandProperty.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Commands/QueryCommand/QueryCommandTests+CommandProperty.swift @@ -27,9 +27,9 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import MistKit -import Testing +internal import Foundation +internal import MistKit +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Commands/QueryCommand/QueryCommandTests+Configuration.swift b/Examples/MistDemo/Tests/MistDemoTests/Commands/QueryCommand/QueryCommandTests+Configuration.swift index 1afa8400..90fed06f 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Commands/QueryCommand/QueryCommandTests+Configuration.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Commands/QueryCommand/QueryCommandTests+Configuration.swift @@ -27,9 +27,9 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import MistKit -import Testing +internal import Foundation +internal import MistKit +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Commands/QueryCommand/QueryCommandTests+ContinuationMarker.swift b/Examples/MistDemo/Tests/MistDemoTests/Commands/QueryCommand/QueryCommandTests+ContinuationMarker.swift index a5273e77..4c768386 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Commands/QueryCommand/QueryCommandTests+ContinuationMarker.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Commands/QueryCommand/QueryCommandTests+ContinuationMarker.swift @@ -27,9 +27,9 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import MistKit -import Testing +internal import Foundation +internal import MistKit +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Commands/QueryCommand/QueryCommandTests+FieldSelection.swift b/Examples/MistDemo/Tests/MistDemoTests/Commands/QueryCommand/QueryCommandTests+FieldSelection.swift index 34cbcf29..020428ce 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Commands/QueryCommand/QueryCommandTests+FieldSelection.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Commands/QueryCommand/QueryCommandTests+FieldSelection.swift @@ -27,9 +27,9 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import MistKit -import Testing +internal import Foundation +internal import MistKit +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Commands/QueryCommand/QueryCommandTests+FilterParsing.swift b/Examples/MistDemo/Tests/MistDemoTests/Commands/QueryCommand/QueryCommandTests+FilterParsing.swift index 5e4f4c6d..f86e3e02 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Commands/QueryCommand/QueryCommandTests+FilterParsing.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Commands/QueryCommand/QueryCommandTests+FilterParsing.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Commands/QueryCommand/QueryCommandTests+LimitValidation.swift b/Examples/MistDemo/Tests/MistDemoTests/Commands/QueryCommand/QueryCommandTests+LimitValidation.swift index 4e496ca1..1bc2b43d 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Commands/QueryCommand/QueryCommandTests+LimitValidation.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Commands/QueryCommand/QueryCommandTests+LimitValidation.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Commands/QueryCommand/QueryCommandTests+MultipleFilters.swift b/Examples/MistDemo/Tests/MistDemoTests/Commands/QueryCommand/QueryCommandTests+MultipleFilters.swift index 0e02e3e4..7868c988 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Commands/QueryCommand/QueryCommandTests+MultipleFilters.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Commands/QueryCommand/QueryCommandTests+MultipleFilters.swift @@ -27,9 +27,9 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import MistKit -import Testing +internal import Foundation +internal import MistKit +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Commands/QueryCommand/QueryCommandTests+ParseFilter.swift b/Examples/MistDemo/Tests/MistDemoTests/Commands/QueryCommand/QueryCommandTests+ParseFilter.swift index 8b664b51..a48170a7 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Commands/QueryCommand/QueryCommandTests+ParseFilter.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Commands/QueryCommand/QueryCommandTests+ParseFilter.swift @@ -27,9 +27,9 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import MistKit -import Testing +internal import Foundation +internal import MistKit +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Commands/QueryCommand/QueryCommandTests+RecordType.swift b/Examples/MistDemo/Tests/MistDemoTests/Commands/QueryCommand/QueryCommandTests+RecordType.swift index 8452cf3b..e94f9f7a 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Commands/QueryCommand/QueryCommandTests+RecordType.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Commands/QueryCommand/QueryCommandTests+RecordType.swift @@ -27,9 +27,9 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import MistKit -import Testing +internal import Foundation +internal import MistKit +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Commands/QueryCommand/QueryCommandTests+SortParsing.swift b/Examples/MistDemo/Tests/MistDemoTests/Commands/QueryCommand/QueryCommandTests+SortParsing.swift index c7b2ea7d..8f1db2e9 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Commands/QueryCommand/QueryCommandTests+SortParsing.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Commands/QueryCommand/QueryCommandTests+SortParsing.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Commands/QueryCommand/QueryCommandTests+ZoneConfiguration.swift b/Examples/MistDemo/Tests/MistDemoTests/Commands/QueryCommand/QueryCommandTests+ZoneConfiguration.swift index 00a43ec5..8d7518c9 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Commands/QueryCommand/QueryCommandTests+ZoneConfiguration.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Commands/QueryCommand/QueryCommandTests+ZoneConfiguration.swift @@ -27,9 +27,9 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import MistKit -import Testing +internal import Foundation +internal import MistKit +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Commands/QueryCommand/QueryCommandTests.swift b/Examples/MistDemo/Tests/MistDemoTests/Commands/QueryCommand/QueryCommandTests.swift index 0dd82ea4..75fec4de 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Commands/QueryCommand/QueryCommandTests.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Commands/QueryCommand/QueryCommandTests.swift @@ -27,7 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Testing +internal import Testing @Suite("QueryCommand") internal enum QueryCommandTests {} diff --git a/Examples/MistDemo/Tests/MistDemoTests/ConfigKeyKit/CommandLineParserTests.swift b/Examples/MistDemo/Tests/MistDemoTests/ConfigKeyKit/CommandLineParserTests.swift index e749446d..d80cd19c 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/ConfigKeyKit/CommandLineParserTests.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/ConfigKeyKit/CommandLineParserTests.swift @@ -27,7 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Testing +internal import Testing @testable import ConfigKeyKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/ConfigKeyKit/CommandRegistry/CommandRegistryTests+AvailableCommands.swift b/Examples/MistDemo/Tests/MistDemoTests/ConfigKeyKit/CommandRegistry/CommandRegistryTests+AvailableCommands.swift index 91b30665..3a8e0582 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/ConfigKeyKit/CommandRegistry/CommandRegistryTests+AvailableCommands.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/ConfigKeyKit/CommandRegistry/CommandRegistryTests+AvailableCommands.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import ConfigKeyKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/ConfigKeyKit/CommandRegistry/CommandRegistryTests+CommandCreation.swift b/Examples/MistDemo/Tests/MistDemoTests/ConfigKeyKit/CommandRegistry/CommandRegistryTests+CommandCreation.swift index 5ae83c2a..7bdc4bc7 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/ConfigKeyKit/CommandRegistry/CommandRegistryTests+CommandCreation.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/ConfigKeyKit/CommandRegistry/CommandRegistryTests+CommandCreation.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import ConfigKeyKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/ConfigKeyKit/CommandRegistry/CommandRegistryTests+CommandTypeRetrieval.swift b/Examples/MistDemo/Tests/MistDemoTests/ConfigKeyKit/CommandRegistry/CommandRegistryTests+CommandTypeRetrieval.swift index 310d985d..b485e82c 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/ConfigKeyKit/CommandRegistry/CommandRegistryTests+CommandTypeRetrieval.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/ConfigKeyKit/CommandRegistry/CommandRegistryTests+CommandTypeRetrieval.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import ConfigKeyKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/ConfigKeyKit/CommandRegistry/CommandRegistryTests+ConcurrentAccess.swift b/Examples/MistDemo/Tests/MistDemoTests/ConfigKeyKit/CommandRegistry/CommandRegistryTests+ConcurrentAccess.swift index 2dcccc11..47d7d990 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/ConfigKeyKit/CommandRegistry/CommandRegistryTests+ConcurrentAccess.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/ConfigKeyKit/CommandRegistry/CommandRegistryTests+ConcurrentAccess.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import ConfigKeyKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/ConfigKeyKit/CommandRegistry/CommandRegistryTests+Errors.swift b/Examples/MistDemo/Tests/MistDemoTests/ConfigKeyKit/CommandRegistry/CommandRegistryTests+Errors.swift index edafa285..bc6b10c8 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/ConfigKeyKit/CommandRegistry/CommandRegistryTests+Errors.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/ConfigKeyKit/CommandRegistry/CommandRegistryTests+Errors.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import ConfigKeyKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/ConfigKeyKit/CommandRegistry/CommandRegistryTests+Metadata.swift b/Examples/MistDemo/Tests/MistDemoTests/ConfigKeyKit/CommandRegistry/CommandRegistryTests+Metadata.swift index 256b1892..19806ad1 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/ConfigKeyKit/CommandRegistry/CommandRegistryTests+Metadata.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/ConfigKeyKit/CommandRegistry/CommandRegistryTests+Metadata.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import ConfigKeyKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/ConfigKeyKit/CommandRegistry/CommandRegistryTests+Overwrite.swift b/Examples/MistDemo/Tests/MistDemoTests/ConfigKeyKit/CommandRegistry/CommandRegistryTests+Overwrite.swift index 3e97ea51..487e7931 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/ConfigKeyKit/CommandRegistry/CommandRegistryTests+Overwrite.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/ConfigKeyKit/CommandRegistry/CommandRegistryTests+Overwrite.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import ConfigKeyKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/ConfigKeyKit/CommandRegistry/CommandRegistryTests+Registration.swift b/Examples/MistDemo/Tests/MistDemoTests/ConfigKeyKit/CommandRegistry/CommandRegistryTests+Registration.swift index c4970088..ccf99b02 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/ConfigKeyKit/CommandRegistry/CommandRegistryTests+Registration.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/ConfigKeyKit/CommandRegistry/CommandRegistryTests+Registration.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import ConfigKeyKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/ConfigKeyKit/CommandRegistry/CommandRegistryTests+TestCommandTypes.swift b/Examples/MistDemo/Tests/MistDemoTests/ConfigKeyKit/CommandRegistry/CommandRegistryTests+TestCommandTypes.swift index f0c6572a..1d86a994 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/ConfigKeyKit/CommandRegistry/CommandRegistryTests+TestCommandTypes.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/ConfigKeyKit/CommandRegistry/CommandRegistryTests+TestCommandTypes.swift @@ -27,7 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation +internal import Foundation @testable import ConfigKeyKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/ConfigKeyKit/CommandRegistry/CommandRegistryTests.swift b/Examples/MistDemo/Tests/MistDemoTests/ConfigKeyKit/CommandRegistry/CommandRegistryTests.swift index d119b1a9..68712d1c 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/ConfigKeyKit/CommandRegistry/CommandRegistryTests.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/ConfigKeyKit/CommandRegistry/CommandRegistryTests.swift @@ -27,7 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Testing +internal import Testing @Suite("CommandRegistry") internal enum CommandRegistryTests {} diff --git a/Examples/MistDemo/Tests/MistDemoTests/Configuration/AuthTokenConfigTests.swift b/Examples/MistDemo/Tests/MistDemoTests/Configuration/AuthTokenConfigTests.swift index 5756dc6e..6632ce32 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Configuration/AuthTokenConfigTests.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Configuration/AuthTokenConfigTests.swift @@ -27,10 +27,10 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Configuration -import Foundation -import MistKit -import Testing +internal import Configuration +internal import Foundation +internal import MistKit +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Configuration/AuthenticationCredentialsTests+ToConfiguration.swift b/Examples/MistDemo/Tests/MistDemoTests/Configuration/AuthenticationCredentialsTests+ToConfiguration.swift index b54071ad..22bd0072 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Configuration/AuthenticationCredentialsTests+ToConfiguration.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Configuration/AuthenticationCredentialsTests+ToConfiguration.swift @@ -27,9 +27,9 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import MistKit -import Testing +internal import Foundation +internal import MistKit +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Configuration/AuthenticationCredentialsTests.swift b/Examples/MistDemo/Tests/MistDemoTests/Configuration/AuthenticationCredentialsTests.swift index bfacb43c..bcec3866 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Configuration/AuthenticationCredentialsTests.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Configuration/AuthenticationCredentialsTests.swift @@ -27,9 +27,9 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import MistKit -import Testing +internal import Foundation +internal import MistKit +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Configuration/CreateConfig/CreateConfigTests+BasicInitialization.swift b/Examples/MistDemo/Tests/MistDemoTests/Configuration/CreateConfig/CreateConfigTests+BasicInitialization.swift index 33abaabd..37b85b4c 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Configuration/CreateConfig/CreateConfigTests+BasicInitialization.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Configuration/CreateConfig/CreateConfigTests+BasicInitialization.swift @@ -27,9 +27,9 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import MistKit -import Testing +internal import Foundation +internal import MistKit +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Configuration/CreateConfig/CreateConfigTests+ComplexInitialization.swift b/Examples/MistDemo/Tests/MistDemoTests/Configuration/CreateConfig/CreateConfigTests+ComplexInitialization.swift index 8586d0e9..9e9e6bf2 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Configuration/CreateConfig/CreateConfigTests+ComplexInitialization.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Configuration/CreateConfig/CreateConfigTests+ComplexInitialization.swift @@ -27,9 +27,9 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import MistKit -import Testing +internal import Foundation +internal import MistKit +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Configuration/CreateConfig/CreateConfigTests+EdgeCases.swift b/Examples/MistDemo/Tests/MistDemoTests/Configuration/CreateConfig/CreateConfigTests+EdgeCases.swift index 2b091d23..5909fb83 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Configuration/CreateConfig/CreateConfigTests+EdgeCases.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Configuration/CreateConfig/CreateConfigTests+EdgeCases.swift @@ -27,9 +27,9 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import MistKit -import Testing +internal import Foundation +internal import MistKit +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Configuration/CreateConfig/CreateConfigTests+FieldInitialization.swift b/Examples/MistDemo/Tests/MistDemoTests/Configuration/CreateConfig/CreateConfigTests+FieldInitialization.swift index 8581d73b..9048cf80 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Configuration/CreateConfig/CreateConfigTests+FieldInitialization.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Configuration/CreateConfig/CreateConfigTests+FieldInitialization.swift @@ -27,9 +27,9 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import MistKit -import Testing +internal import Foundation +internal import MistKit +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Configuration/CreateConfig/CreateConfigTests+OutputFormat.swift b/Examples/MistDemo/Tests/MistDemoTests/Configuration/CreateConfig/CreateConfigTests+OutputFormat.swift index a506616a..be08bcbb 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Configuration/CreateConfig/CreateConfigTests+OutputFormat.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Configuration/CreateConfig/CreateConfigTests+OutputFormat.swift @@ -27,9 +27,9 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import MistKit -import Testing +internal import Foundation +internal import MistKit +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Configuration/CreateConfig/CreateConfigTests.swift b/Examples/MistDemo/Tests/MistDemoTests/Configuration/CreateConfig/CreateConfigTests.swift index ddf83def..a12ba07e 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Configuration/CreateConfig/CreateConfigTests.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Configuration/CreateConfig/CreateConfigTests.swift @@ -27,7 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Testing +internal import Testing @Suite("CreateConfig") internal enum CreateConfigTests {} diff --git a/Examples/MistDemo/Tests/MistDemoTests/Configuration/CurrentUserConfig/CurrentUserConfigTests+BasicInitialization.swift b/Examples/MistDemo/Tests/MistDemoTests/Configuration/CurrentUserConfig/CurrentUserConfigTests+BasicInitialization.swift index e9ac54d6..6fc7cf41 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Configuration/CurrentUserConfig/CurrentUserConfigTests+BasicInitialization.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Configuration/CurrentUserConfig/CurrentUserConfigTests+BasicInitialization.swift @@ -27,9 +27,9 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import MistKit -import Testing +internal import Foundation +internal import MistKit +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Configuration/CurrentUserConfig/CurrentUserConfigTests+ComplexInitialization.swift b/Examples/MistDemo/Tests/MistDemoTests/Configuration/CurrentUserConfig/CurrentUserConfigTests+ComplexInitialization.swift index 2ce98a75..66bcd24e 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Configuration/CurrentUserConfig/CurrentUserConfigTests+ComplexInitialization.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Configuration/CurrentUserConfig/CurrentUserConfigTests+ComplexInitialization.swift @@ -27,9 +27,9 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import MistKit -import Testing +internal import Foundation +internal import MistKit +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Configuration/CurrentUserConfig/CurrentUserConfigTests+EdgeCases.swift b/Examples/MistDemo/Tests/MistDemoTests/Configuration/CurrentUserConfig/CurrentUserConfigTests+EdgeCases.swift index c03dd3fb..73d7e8e4 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Configuration/CurrentUserConfig/CurrentUserConfigTests+EdgeCases.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Configuration/CurrentUserConfig/CurrentUserConfigTests+EdgeCases.swift @@ -27,9 +27,9 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import MistKit -import Testing +internal import Foundation +internal import MistKit +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Configuration/CurrentUserConfig/CurrentUserConfigTests+Fields.swift b/Examples/MistDemo/Tests/MistDemoTests/Configuration/CurrentUserConfig/CurrentUserConfigTests+Fields.swift index c9b26c0a..5ec7cd16 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Configuration/CurrentUserConfig/CurrentUserConfigTests+Fields.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Configuration/CurrentUserConfig/CurrentUserConfigTests+Fields.swift @@ -27,9 +27,9 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import MistKit -import Testing +internal import Foundation +internal import MistKit +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Configuration/CurrentUserConfig/CurrentUserConfigTests+OutputFormat.swift b/Examples/MistDemo/Tests/MistDemoTests/Configuration/CurrentUserConfig/CurrentUserConfigTests+OutputFormat.swift index c1e95a32..b1184c2f 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Configuration/CurrentUserConfig/CurrentUserConfigTests+OutputFormat.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Configuration/CurrentUserConfig/CurrentUserConfigTests+OutputFormat.swift @@ -27,9 +27,9 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import MistKit -import Testing +internal import Foundation +internal import MistKit +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Configuration/CurrentUserConfig/CurrentUserConfigTests.swift b/Examples/MistDemo/Tests/MistDemoTests/Configuration/CurrentUserConfig/CurrentUserConfigTests.swift index 5ef7111d..b9d1f5c8 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Configuration/CurrentUserConfig/CurrentUserConfigTests.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Configuration/CurrentUserConfig/CurrentUserConfigTests.swift @@ -27,7 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Testing +internal import Testing @Suite("CurrentUserConfig") internal enum CurrentUserConfigTests {} diff --git a/Examples/MistDemo/Tests/MistDemoTests/Configuration/DeleteConfigTests.swift b/Examples/MistDemo/Tests/MistDemoTests/Configuration/DeleteConfigTests.swift index 0520e99f..1d721283 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Configuration/DeleteConfigTests.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Configuration/DeleteConfigTests.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Configuration/DeleteErrorTests.swift b/Examples/MistDemo/Tests/MistDemoTests/Configuration/DeleteErrorTests.swift index ab9cbc62..299b5ad2 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Configuration/DeleteErrorTests.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Configuration/DeleteErrorTests.swift @@ -27,7 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Testing +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Configuration/DeleteResultTests.swift b/Examples/MistDemo/Tests/MistDemoTests/Configuration/DeleteResultTests.swift index 2b61be75..3ef42fb8 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Configuration/DeleteResultTests.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Configuration/DeleteResultTests.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Configuration/DemoErrorsConfigTests.swift b/Examples/MistDemo/Tests/MistDemoTests/Configuration/DemoErrorsConfigTests.swift index 1bb52ab6..d50ab7cf 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Configuration/DemoErrorsConfigTests.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Configuration/DemoErrorsConfigTests.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Configuration/FetchChangesConfigTests.swift b/Examples/MistDemo/Tests/MistDemoTests/Configuration/FetchChangesConfigTests.swift index 594ad154..ab048bb7 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Configuration/FetchChangesConfigTests.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Configuration/FetchChangesConfigTests.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Configuration/Field/FieldTests+BasicParsing.swift b/Examples/MistDemo/Tests/MistDemoTests/Configuration/Field/FieldTests+BasicParsing.swift index 2a546e21..bb82d3d4 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Configuration/Field/FieldTests+BasicParsing.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Configuration/Field/FieldTests+BasicParsing.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Configuration/Field/FieldTests+CaseSensitivity.swift b/Examples/MistDemo/Tests/MistDemoTests/Configuration/Field/FieldTests+CaseSensitivity.swift index 279ae0b9..dc9d1f79 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Configuration/Field/FieldTests+CaseSensitivity.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Configuration/Field/FieldTests+CaseSensitivity.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Configuration/Field/FieldTests+ColonHandling.swift b/Examples/MistDemo/Tests/MistDemoTests/Configuration/Field/FieldTests+ColonHandling.swift index f822f2e3..17b4998c 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Configuration/Field/FieldTests+ColonHandling.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Configuration/Field/FieldTests+ColonHandling.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Configuration/Field/FieldTests+EdgeCases.swift b/Examples/MistDemo/Tests/MistDemoTests/Configuration/Field/FieldTests+EdgeCases.swift index 64429ba7..7da8923f 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Configuration/Field/FieldTests+EdgeCases.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Configuration/Field/FieldTests+EdgeCases.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Configuration/Field/FieldTests+ErrorCases.swift b/Examples/MistDemo/Tests/MistDemoTests/Configuration/Field/FieldTests+ErrorCases.swift index e6dd6c5b..46e41049 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Configuration/Field/FieldTests+ErrorCases.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Configuration/Field/FieldTests+ErrorCases.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Configuration/Field/FieldTests+ParseMultiple.swift b/Examples/MistDemo/Tests/MistDemoTests/Configuration/Field/FieldTests+ParseMultiple.swift index bbedf8ea..734bd5dd 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Configuration/Field/FieldTests+ParseMultiple.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Configuration/Field/FieldTests+ParseMultiple.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Configuration/Field/FieldTests+WhitespaceHandling.swift b/Examples/MistDemo/Tests/MistDemoTests/Configuration/Field/FieldTests+WhitespaceHandling.swift index 8d5a9537..f5975855 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Configuration/Field/FieldTests+WhitespaceHandling.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Configuration/Field/FieldTests+WhitespaceHandling.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Configuration/Field/FieldTests.swift b/Examples/MistDemo/Tests/MistDemoTests/Configuration/Field/FieldTests.swift index 2ba8acff..0c9c06bf 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Configuration/Field/FieldTests.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Configuration/Field/FieldTests.swift @@ -27,7 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Testing +internal import Testing @Suite("Field Parsing") internal enum FieldTests {} diff --git a/Examples/MistDemo/Tests/MistDemoTests/Configuration/FieldParsingError/FieldParsingErrorTests+EmptyFieldName.swift b/Examples/MistDemo/Tests/MistDemoTests/Configuration/FieldParsingError/FieldParsingErrorTests+EmptyFieldName.swift index 4436c94b..ec570cf3 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Configuration/FieldParsingError/FieldParsingErrorTests+EmptyFieldName.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Configuration/FieldParsingError/FieldParsingErrorTests+EmptyFieldName.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Configuration/FieldParsingError/FieldParsingErrorTests+InvalidFormat.swift b/Examples/MistDemo/Tests/MistDemoTests/Configuration/FieldParsingError/FieldParsingErrorTests+InvalidFormat.swift index 8b4ca785..ed2f7ff0 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Configuration/FieldParsingError/FieldParsingErrorTests+InvalidFormat.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Configuration/FieldParsingError/FieldParsingErrorTests+InvalidFormat.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Configuration/FieldParsingError/FieldParsingErrorTests+InvalidValueForType.swift b/Examples/MistDemo/Tests/MistDemoTests/Configuration/FieldParsingError/FieldParsingErrorTests+InvalidValueForType.swift index 2e1cef4a..0cc58ec4 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Configuration/FieldParsingError/FieldParsingErrorTests+InvalidValueForType.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Configuration/FieldParsingError/FieldParsingErrorTests+InvalidValueForType.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Configuration/FieldParsingError/FieldParsingErrorTests+UnknownFieldType.swift b/Examples/MistDemo/Tests/MistDemoTests/Configuration/FieldParsingError/FieldParsingErrorTests+UnknownFieldType.swift index 05f4d57b..2583a0b8 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Configuration/FieldParsingError/FieldParsingErrorTests+UnknownFieldType.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Configuration/FieldParsingError/FieldParsingErrorTests+UnknownFieldType.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Configuration/FieldParsingError/FieldParsingErrorTests+UnsupportedFieldType.swift b/Examples/MistDemo/Tests/MistDemoTests/Configuration/FieldParsingError/FieldParsingErrorTests+UnsupportedFieldType.swift index 946c6ca5..ce6e9566 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Configuration/FieldParsingError/FieldParsingErrorTests+UnsupportedFieldType.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Configuration/FieldParsingError/FieldParsingErrorTests+UnsupportedFieldType.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Configuration/FieldParsingError/FieldParsingErrorTests.swift b/Examples/MistDemo/Tests/MistDemoTests/Configuration/FieldParsingError/FieldParsingErrorTests.swift index 0358867f..9fae6237 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Configuration/FieldParsingError/FieldParsingErrorTests.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Configuration/FieldParsingError/FieldParsingErrorTests.swift @@ -27,7 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Testing +internal import Testing @Suite("FieldParsingError LocalizedError") internal enum FieldParsingErrorTests {} diff --git a/Examples/MistDemo/Tests/MistDemoTests/Configuration/FieldType/FieldTypeTests+DoubleConversion.swift b/Examples/MistDemo/Tests/MistDemoTests/Configuration/FieldType/FieldTypeTests+DoubleConversion.swift index c58c245c..294fa60e 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Configuration/FieldType/FieldTypeTests+DoubleConversion.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Configuration/FieldType/FieldTypeTests+DoubleConversion.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Configuration/FieldType/FieldTypeTests+EnumProperties.swift b/Examples/MistDemo/Tests/MistDemoTests/Configuration/FieldType/FieldTypeTests+EnumProperties.swift index 5afa6f72..7e23f58a 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Configuration/FieldType/FieldTypeTests+EnumProperties.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Configuration/FieldType/FieldTypeTests+EnumProperties.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Configuration/FieldType/FieldTypeTests+Int64Conversion.swift b/Examples/MistDemo/Tests/MistDemoTests/Configuration/FieldType/FieldTypeTests+Int64Conversion.swift index 19b3e899..4bdf4daa 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Configuration/FieldType/FieldTypeTests+Int64Conversion.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Configuration/FieldType/FieldTypeTests+Int64Conversion.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Configuration/FieldType/FieldTypeTests+StringConversion.swift b/Examples/MistDemo/Tests/MistDemoTests/Configuration/FieldType/FieldTypeTests+StringConversion.swift index ee50995e..d1630946 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Configuration/FieldType/FieldTypeTests+StringConversion.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Configuration/FieldType/FieldTypeTests+StringConversion.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Configuration/FieldType/FieldTypeTests+TimestampConversion.swift b/Examples/MistDemo/Tests/MistDemoTests/Configuration/FieldType/FieldTypeTests+TimestampConversion.swift index ba648c28..aaf7bbbc 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Configuration/FieldType/FieldTypeTests+TimestampConversion.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Configuration/FieldType/FieldTypeTests+TimestampConversion.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Configuration/FieldType/FieldTypeTests+UnsupportedType.swift b/Examples/MistDemo/Tests/MistDemoTests/Configuration/FieldType/FieldTypeTests+UnsupportedType.swift index e05de3ca..0ce7ef06 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Configuration/FieldType/FieldTypeTests+UnsupportedType.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Configuration/FieldType/FieldTypeTests+UnsupportedType.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Configuration/FieldType/FieldTypeTests.swift b/Examples/MistDemo/Tests/MistDemoTests/Configuration/FieldType/FieldTypeTests.swift index 25a4d442..d7119081 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Configuration/FieldType/FieldTypeTests.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Configuration/FieldType/FieldTypeTests.swift @@ -27,7 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Testing +internal import Testing @Suite("FieldType Conversion") internal enum FieldTypeTests {} diff --git a/Examples/MistDemo/Tests/MistDemoTests/Configuration/LookupConfigTests.swift b/Examples/MistDemo/Tests/MistDemoTests/Configuration/LookupConfigTests.swift index 80e16cbd..7c4278d5 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Configuration/LookupConfigTests.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Configuration/LookupConfigTests.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Configuration/LookupErrorTests.swift b/Examples/MistDemo/Tests/MistDemoTests/Configuration/LookupErrorTests.swift index 3ae0dc22..5a02af15 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Configuration/LookupErrorTests.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Configuration/LookupErrorTests.swift @@ -27,7 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Testing +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Configuration/LookupZonesConfigTests.swift b/Examples/MistDemo/Tests/MistDemoTests/Configuration/LookupZonesConfigTests.swift index 4cceae3c..bdc26152 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Configuration/LookupZonesConfigTests.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Configuration/LookupZonesConfigTests.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Configuration/MistDemoConfigTests.swift b/Examples/MistDemo/Tests/MistDemoTests/Configuration/MistDemoConfigTests.swift index a360b232..32138f1c 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Configuration/MistDemoConfigTests.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Configuration/MistDemoConfigTests.swift @@ -27,9 +27,9 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import MistKit -import Testing +internal import Foundation +internal import MistKit +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Configuration/ModifyConfigParsingTests.swift b/Examples/MistDemo/Tests/MistDemoTests/Configuration/ModifyConfigParsingTests.swift index bc5c2adb..2d226a20 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Configuration/ModifyConfigParsingTests.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Configuration/ModifyConfigParsingTests.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Configuration/ModifyConfigTests.swift b/Examples/MistDemo/Tests/MistDemoTests/Configuration/ModifyConfigTests.swift index 280b87bb..11515fee 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Configuration/ModifyConfigTests.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Configuration/ModifyConfigTests.swift @@ -27,7 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Testing +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Configuration/ModifyErrorTests.swift b/Examples/MistDemo/Tests/MistDemoTests/Configuration/ModifyErrorTests.swift index 0d027eb0..5f1c0768 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Configuration/ModifyErrorTests.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Configuration/ModifyErrorTests.swift @@ -27,7 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Testing +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Configuration/ModifyOperationInputTests.swift b/Examples/MistDemo/Tests/MistDemoTests/Configuration/ModifyOperationInputTests.swift index b3503379..e208685b 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Configuration/ModifyOperationInputTests.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Configuration/ModifyOperationInputTests.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import MistKit -import Testing +internal import MistKit +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Configuration/QueryConfig/QueryConfigTests+BasicInitialization.swift b/Examples/MistDemo/Tests/MistDemoTests/Configuration/QueryConfig/QueryConfigTests+BasicInitialization.swift index c5109eef..8b5c1ee5 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Configuration/QueryConfig/QueryConfigTests+BasicInitialization.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Configuration/QueryConfig/QueryConfigTests+BasicInitialization.swift @@ -27,9 +27,9 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import MistKit -import Testing +internal import Foundation +internal import MistKit +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Configuration/QueryConfig/QueryConfigTests+ComplexInitialization.swift b/Examples/MistDemo/Tests/MistDemoTests/Configuration/QueryConfig/QueryConfigTests+ComplexInitialization.swift index debd32bb..46610ad1 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Configuration/QueryConfig/QueryConfigTests+ComplexInitialization.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Configuration/QueryConfig/QueryConfigTests+ComplexInitialization.swift @@ -27,9 +27,9 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import MistKit -import Testing +internal import Foundation +internal import MistKit +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Configuration/QueryConfig/QueryConfigTests+ContinuationMarker.swift b/Examples/MistDemo/Tests/MistDemoTests/Configuration/QueryConfig/QueryConfigTests+ContinuationMarker.swift index 1c1f5908..29a09a4e 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Configuration/QueryConfig/QueryConfigTests+ContinuationMarker.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Configuration/QueryConfig/QueryConfigTests+ContinuationMarker.swift @@ -27,9 +27,9 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import MistKit -import Testing +internal import Foundation +internal import MistKit +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Configuration/QueryConfig/QueryConfigTests+EdgeCases.swift b/Examples/MistDemo/Tests/MistDemoTests/Configuration/QueryConfig/QueryConfigTests+EdgeCases.swift index 3f4e6a3a..b4234e27 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Configuration/QueryConfig/QueryConfigTests+EdgeCases.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Configuration/QueryConfig/QueryConfigTests+EdgeCases.swift @@ -27,9 +27,9 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import MistKit -import Testing +internal import Foundation +internal import MistKit +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Configuration/QueryConfig/QueryConfigTests+FieldsFilter.swift b/Examples/MistDemo/Tests/MistDemoTests/Configuration/QueryConfig/QueryConfigTests+FieldsFilter.swift index 3826e085..09a9d9de 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Configuration/QueryConfig/QueryConfigTests+FieldsFilter.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Configuration/QueryConfig/QueryConfigTests+FieldsFilter.swift @@ -27,9 +27,9 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import MistKit -import Testing +internal import Foundation +internal import MistKit +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Configuration/QueryConfig/QueryConfigTests+Filter.swift b/Examples/MistDemo/Tests/MistDemoTests/Configuration/QueryConfig/QueryConfigTests+Filter.swift index 76102eb7..72ad7567 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Configuration/QueryConfig/QueryConfigTests+Filter.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Configuration/QueryConfig/QueryConfigTests+Filter.swift @@ -27,9 +27,9 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import MistKit -import Testing +internal import Foundation +internal import MistKit +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Configuration/QueryConfig/QueryConfigTests+Limit.swift b/Examples/MistDemo/Tests/MistDemoTests/Configuration/QueryConfig/QueryConfigTests+Limit.swift index 7049f320..88c7ff7b 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Configuration/QueryConfig/QueryConfigTests+Limit.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Configuration/QueryConfig/QueryConfigTests+Limit.swift @@ -27,9 +27,9 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import MistKit -import Testing +internal import Foundation +internal import MistKit +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Configuration/QueryConfig/QueryConfigTests+Offset.swift b/Examples/MistDemo/Tests/MistDemoTests/Configuration/QueryConfig/QueryConfigTests+Offset.swift index 71911f81..784d9d88 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Configuration/QueryConfig/QueryConfigTests+Offset.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Configuration/QueryConfig/QueryConfigTests+Offset.swift @@ -27,9 +27,9 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import MistKit -import Testing +internal import Foundation +internal import MistKit +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Configuration/QueryConfig/QueryConfigTests+OutputFormat.swift b/Examples/MistDemo/Tests/MistDemoTests/Configuration/QueryConfig/QueryConfigTests+OutputFormat.swift index d4ce5e93..d8d9b1cb 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Configuration/QueryConfig/QueryConfigTests+OutputFormat.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Configuration/QueryConfig/QueryConfigTests+OutputFormat.swift @@ -27,9 +27,9 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import MistKit -import Testing +internal import Foundation +internal import MistKit +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Configuration/QueryConfig/QueryConfigTests+SortOption.swift b/Examples/MistDemo/Tests/MistDemoTests/Configuration/QueryConfig/QueryConfigTests+SortOption.swift index d4490ab8..25bd3d12 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Configuration/QueryConfig/QueryConfigTests+SortOption.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Configuration/QueryConfig/QueryConfigTests+SortOption.swift @@ -27,9 +27,9 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import MistKit -import Testing +internal import Foundation +internal import MistKit +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Configuration/QueryConfig/QueryConfigTests.swift b/Examples/MistDemo/Tests/MistDemoTests/Configuration/QueryConfig/QueryConfigTests.swift index a1d9bfac..cc9b5c98 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Configuration/QueryConfig/QueryConfigTests.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Configuration/QueryConfig/QueryConfigTests.swift @@ -27,7 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Testing +internal import Testing @Suite("QueryConfig") internal enum QueryConfigTests {} diff --git a/Examples/MistDemo/Tests/MistDemoTests/Configuration/TestPrivateConfigTests.swift b/Examples/MistDemo/Tests/MistDemoTests/Configuration/TestPrivateConfigTests.swift index 16155ba3..44e1f26d 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Configuration/TestPrivateConfigTests.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Configuration/TestPrivateConfigTests.swift @@ -27,9 +27,9 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import MistKit -import Testing +internal import Foundation +internal import MistKit +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Configuration/TestPublicConfigTests.swift b/Examples/MistDemo/Tests/MistDemoTests/Configuration/TestPublicConfigTests.swift index 69451f7c..00ccddae 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Configuration/TestPublicConfigTests.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Configuration/TestPublicConfigTests.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Configuration/UpdateConfig/UpdateConfigTests+BasicInitialization.swift b/Examples/MistDemo/Tests/MistDemoTests/Configuration/UpdateConfig/UpdateConfigTests+BasicInitialization.swift index 2f02d39f..62acf81e 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Configuration/UpdateConfig/UpdateConfigTests+BasicInitialization.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Configuration/UpdateConfig/UpdateConfigTests+BasicInitialization.swift @@ -27,9 +27,9 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import MistKit -import Testing +internal import Foundation +internal import MistKit +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Configuration/UpdateConfig/UpdateConfigTests+CombinedEdgeCases.swift b/Examples/MistDemo/Tests/MistDemoTests/Configuration/UpdateConfig/UpdateConfigTests+CombinedEdgeCases.swift index 09254f22..a5c436c0 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Configuration/UpdateConfig/UpdateConfigTests+CombinedEdgeCases.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Configuration/UpdateConfig/UpdateConfigTests+CombinedEdgeCases.swift @@ -27,9 +27,9 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import MistKit -import Testing +internal import Foundation +internal import MistKit +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Configuration/UpdateConfig/UpdateConfigTests+FieldInitialization.swift b/Examples/MistDemo/Tests/MistDemoTests/Configuration/UpdateConfig/UpdateConfigTests+FieldInitialization.swift index 2c737440..a708ceb9 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Configuration/UpdateConfig/UpdateConfigTests+FieldInitialization.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Configuration/UpdateConfig/UpdateConfigTests+FieldInitialization.swift @@ -27,9 +27,9 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import MistKit -import Testing +internal import Foundation +internal import MistKit +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Configuration/UpdateConfig/UpdateConfigTests+ForceFlag.swift b/Examples/MistDemo/Tests/MistDemoTests/Configuration/UpdateConfig/UpdateConfigTests+ForceFlag.swift index 7fdfaad7..82146f32 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Configuration/UpdateConfig/UpdateConfigTests+ForceFlag.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Configuration/UpdateConfig/UpdateConfigTests+ForceFlag.swift @@ -27,9 +27,9 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import MistKit -import Testing +internal import Foundation +internal import MistKit +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Configuration/UpdateConfig/UpdateConfigTests+OutputFormat.swift b/Examples/MistDemo/Tests/MistDemoTests/Configuration/UpdateConfig/UpdateConfigTests+OutputFormat.swift index f88afed6..695063dc 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Configuration/UpdateConfig/UpdateConfigTests+OutputFormat.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Configuration/UpdateConfig/UpdateConfigTests+OutputFormat.swift @@ -27,9 +27,9 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import MistKit -import Testing +internal import Foundation +internal import MistKit +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Configuration/UpdateConfig/UpdateConfigTests.swift b/Examples/MistDemo/Tests/MistDemoTests/Configuration/UpdateConfig/UpdateConfigTests.swift index 755c2ccd..b4a103eb 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Configuration/UpdateConfig/UpdateConfigTests.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Configuration/UpdateConfig/UpdateConfigTests.swift @@ -27,7 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Testing +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Configuration/UpdateConfig/UpdateErrorTests.swift b/Examples/MistDemo/Tests/MistDemoTests/Configuration/UpdateConfig/UpdateErrorTests.swift index 5be62b49..7acb113d 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Configuration/UpdateConfig/UpdateErrorTests.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Configuration/UpdateConfig/UpdateErrorTests.swift @@ -27,7 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Testing +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Configuration/UploadAssetConfigTests.swift b/Examples/MistDemo/Tests/MistDemoTests/Configuration/UploadAssetConfigTests.swift index cecb0092..459c7d0e 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Configuration/UploadAssetConfigTests.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Configuration/UploadAssetConfigTests.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Errors/CreateError/CreateErrorTests+ErrorDescription.swift b/Examples/MistDemo/Tests/MistDemoTests/Errors/CreateError/CreateErrorTests+ErrorDescription.swift index 4a9eb718..cac1e996 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Errors/CreateError/CreateErrorTests+ErrorDescription.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Errors/CreateError/CreateErrorTests+ErrorDescription.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Errors/CreateError/CreateErrorTests+ErrorMessageContent.swift b/Examples/MistDemo/Tests/MistDemoTests/Errors/CreateError/CreateErrorTests+ErrorMessageContent.swift index dd350026..902df3fb 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Errors/CreateError/CreateErrorTests+ErrorMessageContent.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Errors/CreateError/CreateErrorTests+ErrorMessageContent.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Errors/CreateError/CreateErrorTests+ErrorThrowing.swift b/Examples/MistDemo/Tests/MistDemoTests/Errors/CreateError/CreateErrorTests+ErrorThrowing.swift index 7df9747c..3acc0085 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Errors/CreateError/CreateErrorTests+ErrorThrowing.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Errors/CreateError/CreateErrorTests+ErrorThrowing.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Errors/CreateError/CreateErrorTests+LocalizedErrorConformance.swift b/Examples/MistDemo/Tests/MistDemoTests/Errors/CreateError/CreateErrorTests+LocalizedErrorConformance.swift index a0536195..abc601dd 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Errors/CreateError/CreateErrorTests+LocalizedErrorConformance.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Errors/CreateError/CreateErrorTests+LocalizedErrorConformance.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Errors/CreateError/CreateErrorTests.swift b/Examples/MistDemo/Tests/MistDemoTests/Errors/CreateError/CreateErrorTests.swift index 5c6b94e9..54c99f58 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Errors/CreateError/CreateErrorTests.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Errors/CreateError/CreateErrorTests.swift @@ -27,7 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Testing +internal import Testing @Suite("CreateError") internal enum CreateErrorTests {} diff --git a/Examples/MistDemo/Tests/MistDemoTests/Errors/CurrentUserErrorTests.swift b/Examples/MistDemo/Tests/MistDemoTests/Errors/CurrentUserErrorTests.swift index 8ca42175..aa7d1ef0 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Errors/CurrentUserErrorTests.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Errors/CurrentUserErrorTests.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Errors/ErrorOutputTests.swift b/Examples/MistDemo/Tests/MistDemoTests/Errors/ErrorOutputTests.swift index dcd2c5bd..e6fc01b2 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Errors/ErrorOutputTests.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Errors/ErrorOutputTests.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Errors/MistDemoError/MistDemoErrorTests+ErrorCode.swift b/Examples/MistDemo/Tests/MistDemoTests/Errors/MistDemoError/MistDemoErrorTests+ErrorCode.swift index 9f5bb3dc..d059f16b 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Errors/MistDemoError/MistDemoErrorTests+ErrorCode.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Errors/MistDemoError/MistDemoErrorTests+ErrorCode.swift @@ -27,9 +27,9 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import MistKit -import Testing +internal import Foundation +internal import MistKit +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Errors/MistDemoError/MistDemoErrorTests+ErrorDescription.swift b/Examples/MistDemo/Tests/MistDemoTests/Errors/MistDemoError/MistDemoErrorTests+ErrorDescription.swift index e22519e2..254bb1a9 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Errors/MistDemoError/MistDemoErrorTests+ErrorDescription.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Errors/MistDemoError/MistDemoErrorTests+ErrorDescription.swift @@ -27,9 +27,9 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import MistKit -import Testing +internal import Foundation +internal import MistKit +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Errors/MistDemoError/MistDemoErrorTests+ErrorDetails.swift b/Examples/MistDemo/Tests/MistDemoTests/Errors/MistDemoError/MistDemoErrorTests+ErrorDetails.swift index ad638450..bd61a2b4 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Errors/MistDemoError/MistDemoErrorTests+ErrorDetails.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Errors/MistDemoError/MistDemoErrorTests+ErrorDetails.swift @@ -27,9 +27,9 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import MistKit -import Testing +internal import Foundation +internal import MistKit +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Errors/MistDemoError/MistDemoErrorTests+ErrorOutputConversion.swift b/Examples/MistDemo/Tests/MistDemoTests/Errors/MistDemoError/MistDemoErrorTests+ErrorOutputConversion.swift index ec5a6680..a8f997ca 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Errors/MistDemoError/MistDemoErrorTests+ErrorOutputConversion.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Errors/MistDemoError/MistDemoErrorTests+ErrorOutputConversion.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Errors/MistDemoError/MistDemoErrorTests+RecoverySuggestion.swift b/Examples/MistDemo/Tests/MistDemoTests/Errors/MistDemoError/MistDemoErrorTests+RecoverySuggestion.swift index b0334b42..a0cfec3f 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Errors/MistDemoError/MistDemoErrorTests+RecoverySuggestion.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Errors/MistDemoError/MistDemoErrorTests+RecoverySuggestion.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Errors/MistDemoError/MistDemoErrorTests.swift b/Examples/MistDemo/Tests/MistDemoTests/Errors/MistDemoError/MistDemoErrorTests.swift index 0578039c..f194e596 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Errors/MistDemoError/MistDemoErrorTests.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Errors/MistDemoError/MistDemoErrorTests.swift @@ -27,7 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Testing +internal import Testing @Suite("MistDemoError") internal enum MistDemoErrorTests {} diff --git a/Examples/MistDemo/Tests/MistDemoTests/Errors/QueryErrorTests.swift b/Examples/MistDemo/Tests/MistDemoTests/Errors/QueryErrorTests.swift index fc0d34a9..72a3291e 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Errors/QueryErrorTests.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Errors/QueryErrorTests.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Extensions/ConfigKey+MistDemo/ConfigKey+MistDemoTests+BooleanConfigKeyWithPrefix.swift b/Examples/MistDemo/Tests/MistDemoTests/Extensions/ConfigKey+MistDemo/ConfigKey+MistDemoTests+BooleanConfigKeyWithPrefix.swift index 883e3df6..40cdf357 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Extensions/ConfigKey+MistDemo/ConfigKey+MistDemoTests+BooleanConfigKeyWithPrefix.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Extensions/ConfigKey+MistDemo/ConfigKey+MistDemoTests+BooleanConfigKeyWithPrefix.swift @@ -27,9 +27,9 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import ConfigKeyKit -import Foundation -import Testing +internal import ConfigKeyKit +internal import Foundation +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Extensions/ConfigKey+MistDemo/ConfigKey+MistDemoTests+ConfigKeyWithPrefix.swift b/Examples/MistDemo/Tests/MistDemoTests/Extensions/ConfigKey+MistDemo/ConfigKey+MistDemoTests+ConfigKeyWithPrefix.swift index 5e7abd83..c0561d18 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Extensions/ConfigKey+MistDemo/ConfigKey+MistDemoTests+ConfigKeyWithPrefix.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Extensions/ConfigKey+MistDemo/ConfigKey+MistDemoTests+ConfigKeyWithPrefix.swift @@ -27,9 +27,9 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import ConfigKeyKit -import Foundation -import Testing +internal import ConfigKeyKit +internal import Foundation +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Extensions/ConfigKey+MistDemo/ConfigKey+MistDemoTests+EdgeCases.swift b/Examples/MistDemo/Tests/MistDemoTests/Extensions/ConfigKey+MistDemo/ConfigKey+MistDemoTests+EdgeCases.swift index ad2a0523..a68d69b1 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Extensions/ConfigKey+MistDemo/ConfigKey+MistDemoTests+EdgeCases.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Extensions/ConfigKey+MistDemo/ConfigKey+MistDemoTests+EdgeCases.swift @@ -27,9 +27,9 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import ConfigKeyKit -import Foundation -import Testing +internal import ConfigKeyKit +internal import Foundation +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Extensions/ConfigKey+MistDemo/ConfigKey+MistDemoTests+OptionalConfigKeyWithPrefix.swift b/Examples/MistDemo/Tests/MistDemoTests/Extensions/ConfigKey+MistDemo/ConfigKey+MistDemoTests+OptionalConfigKeyWithPrefix.swift index 1aa9cbf2..d59bf321 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Extensions/ConfigKey+MistDemo/ConfigKey+MistDemoTests+OptionalConfigKeyWithPrefix.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Extensions/ConfigKey+MistDemo/ConfigKey+MistDemoTests+OptionalConfigKeyWithPrefix.swift @@ -27,9 +27,9 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import ConfigKeyKit -import Foundation -import Testing +internal import ConfigKeyKit +internal import Foundation +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Extensions/ConfigKey+MistDemo/ConfigKey+MistDemoTests+RealWorldUsage.swift b/Examples/MistDemo/Tests/MistDemoTests/Extensions/ConfigKey+MistDemo/ConfigKey+MistDemoTests+RealWorldUsage.swift index d3804841..3f33b246 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Extensions/ConfigKey+MistDemo/ConfigKey+MistDemoTests+RealWorldUsage.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Extensions/ConfigKey+MistDemo/ConfigKey+MistDemoTests+RealWorldUsage.swift @@ -27,9 +27,9 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import ConfigKeyKit -import Foundation -import Testing +internal import ConfigKeyKit +internal import Foundation +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Extensions/ConfigKey+MistDemo/ConfigKey+MistDemoTests.swift b/Examples/MistDemo/Tests/MistDemoTests/Extensions/ConfigKey+MistDemo/ConfigKey+MistDemoTests.swift index 73e08030..3a7efc27 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Extensions/ConfigKey+MistDemo/ConfigKey+MistDemoTests.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Extensions/ConfigKey+MistDemo/ConfigKey+MistDemoTests.swift @@ -27,7 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Testing +internal import Testing @Suite("ConfigKey+MistDemo") internal enum ConfigKeyMistDemoTests {} diff --git a/Examples/MistDemo/Tests/MistDemoTests/Extensions/FieldValue+FieldType/FieldValue+FieldTypeTests+BytesType.swift b/Examples/MistDemo/Tests/MistDemoTests/Extensions/FieldValue+FieldType/FieldValue+FieldTypeTests+BytesType.swift index 3dc22cce..79333ed4 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Extensions/FieldValue+FieldType/FieldValue+FieldTypeTests+BytesType.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Extensions/FieldValue+FieldType/FieldValue+FieldTypeTests+BytesType.swift @@ -27,9 +27,9 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import MistKit -import Testing +internal import Foundation +internal import MistKit +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Extensions/FieldValue+FieldType/FieldValue+FieldTypeTests+DoubleType.swift b/Examples/MistDemo/Tests/MistDemoTests/Extensions/FieldValue+FieldType/FieldValue+FieldTypeTests+DoubleType.swift index 64dce7a3..50ec0a66 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Extensions/FieldValue+FieldType/FieldValue+FieldTypeTests+DoubleType.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Extensions/FieldValue+FieldType/FieldValue+FieldTypeTests+DoubleType.swift @@ -27,9 +27,9 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import MistKit -import Testing +internal import Foundation +internal import MistKit +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Extensions/FieldValue+FieldType/FieldValue+FieldTypeTests+Int64Type.swift b/Examples/MistDemo/Tests/MistDemoTests/Extensions/FieldValue+FieldType/FieldValue+FieldTypeTests+Int64Type.swift index 1b2a7503..fd4a0ca6 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Extensions/FieldValue+FieldType/FieldValue+FieldTypeTests+Int64Type.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Extensions/FieldValue+FieldType/FieldValue+FieldTypeTests+Int64Type.swift @@ -27,9 +27,9 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import MistKit -import Testing +internal import Foundation +internal import MistKit +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Extensions/FieldValue+FieldType/FieldValue+FieldTypeTests+InvalidTypeConversion.swift b/Examples/MistDemo/Tests/MistDemoTests/Extensions/FieldValue+FieldType/FieldValue+FieldTypeTests+InvalidTypeConversion.swift index c9a8c4b9..5267cd70 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Extensions/FieldValue+FieldType/FieldValue+FieldTypeTests+InvalidTypeConversion.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Extensions/FieldValue+FieldType/FieldValue+FieldTypeTests+InvalidTypeConversion.swift @@ -27,9 +27,9 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import MistKit -import Testing +internal import Foundation +internal import MistKit +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Extensions/FieldValue+FieldType/FieldValue+FieldTypeTests+StringType.swift b/Examples/MistDemo/Tests/MistDemoTests/Extensions/FieldValue+FieldType/FieldValue+FieldTypeTests+StringType.swift index 05a364e6..dbff1450 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Extensions/FieldValue+FieldType/FieldValue+FieldTypeTests+StringType.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Extensions/FieldValue+FieldType/FieldValue+FieldTypeTests+StringType.swift @@ -27,9 +27,9 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import MistKit -import Testing +internal import Foundation +internal import MistKit +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Extensions/FieldValue+FieldType/FieldValue+FieldTypeTests+TimestampDateType.swift b/Examples/MistDemo/Tests/MistDemoTests/Extensions/FieldValue+FieldType/FieldValue+FieldTypeTests+TimestampDateType.swift index fdef00bc..814f4523 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Extensions/FieldValue+FieldType/FieldValue+FieldTypeTests+TimestampDateType.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Extensions/FieldValue+FieldType/FieldValue+FieldTypeTests+TimestampDateType.swift @@ -27,9 +27,9 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import MistKit -import Testing +internal import Foundation +internal import MistKit +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Extensions/FieldValue+FieldType/FieldValue+FieldTypeTests+UnsupportedType.swift b/Examples/MistDemo/Tests/MistDemoTests/Extensions/FieldValue+FieldType/FieldValue+FieldTypeTests+UnsupportedType.swift index 1b2d2e2a..e192a836 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Extensions/FieldValue+FieldType/FieldValue+FieldTypeTests+UnsupportedType.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Extensions/FieldValue+FieldType/FieldValue+FieldTypeTests+UnsupportedType.swift @@ -27,9 +27,9 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import MistKit -import Testing +internal import Foundation +internal import MistKit +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Extensions/FieldValue+FieldType/FieldValue+FieldTypeTests.swift b/Examples/MistDemo/Tests/MistDemoTests/Extensions/FieldValue+FieldType/FieldValue+FieldTypeTests.swift index 0fee26b3..5115fb82 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Extensions/FieldValue+FieldType/FieldValue+FieldTypeTests.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Extensions/FieldValue+FieldType/FieldValue+FieldTypeTests.swift @@ -27,7 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Testing +internal import Testing @Suite("FieldValue+FieldType Initialization") internal enum FieldValueFieldTypeTests {} diff --git a/Examples/MistDemo/Tests/MistDemoTests/MistDemoConfig+Testing.swift b/Examples/MistDemo/Tests/MistDemoTests/MistDemoConfig+Testing.swift index 75c90c3f..8cd6adae 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/MistDemoConfig+Testing.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/MistDemoConfig+Testing.swift @@ -27,9 +27,9 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Configuration -import Foundation -import MistKit +internal import Configuration +internal import Foundation +internal import MistKit @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/CSVEscaper/CSVEscaperTests+Combination.swift b/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/CSVEscaper/CSVEscaperTests+Combination.swift index f20f419a..caf71e92 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/CSVEscaper/CSVEscaperTests+Combination.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/CSVEscaper/CSVEscaperTests+Combination.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/CSVEscaper/CSVEscaperTests+CommaEscaping.swift b/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/CSVEscaper/CSVEscaperTests+CommaEscaping.swift index 1adbcfc0..9d09d496 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/CSVEscaper/CSVEscaperTests+CommaEscaping.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/CSVEscaper/CSVEscaperTests+CommaEscaping.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/CSVEscaper/CSVEscaperTests+EdgeCases.swift b/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/CSVEscaper/CSVEscaperTests+EdgeCases.swift index ac26f84c..02df80c5 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/CSVEscaper/CSVEscaperTests+EdgeCases.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/CSVEscaper/CSVEscaperTests+EdgeCases.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/CSVEscaper/CSVEscaperTests+NewlineEscaping.swift b/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/CSVEscaper/CSVEscaperTests+NewlineEscaping.swift index b2574fb5..8c0ca27e 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/CSVEscaper/CSVEscaperTests+NewlineEscaping.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/CSVEscaper/CSVEscaperTests+NewlineEscaping.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/CSVEscaper/CSVEscaperTests+PlainString.swift b/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/CSVEscaper/CSVEscaperTests+PlainString.swift index 9cb6fcc2..5c6dba04 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/CSVEscaper/CSVEscaperTests+PlainString.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/CSVEscaper/CSVEscaperTests+PlainString.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/CSVEscaper/CSVEscaperTests+QuoteEscaping.swift b/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/CSVEscaper/CSVEscaperTests+QuoteEscaping.swift index 453f7878..4da58221 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/CSVEscaper/CSVEscaperTests+QuoteEscaping.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/CSVEscaper/CSVEscaperTests+QuoteEscaping.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/CSVEscaper/CSVEscaperTests+TabEscaping.swift b/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/CSVEscaper/CSVEscaperTests+TabEscaping.swift index f7da7c3b..8d59f761 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/CSVEscaper/CSVEscaperTests+TabEscaping.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/CSVEscaper/CSVEscaperTests+TabEscaping.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/CSVEscaper/CSVEscaperTests+UnicodeAndEmoji.swift b/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/CSVEscaper/CSVEscaperTests+UnicodeAndEmoji.swift index e0c937ab..316e998f 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/CSVEscaper/CSVEscaperTests+UnicodeAndEmoji.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/CSVEscaper/CSVEscaperTests+UnicodeAndEmoji.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/CSVEscaper/CSVEscaperTests.swift b/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/CSVEscaper/CSVEscaperTests.swift index 63a892d9..ee4b3af2 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/CSVEscaper/CSVEscaperTests.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/CSVEscaper/CSVEscaperTests.swift @@ -27,7 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Testing +internal import Testing @Suite("CSVEscaper - RFC 4180 Compliance") internal enum CSVEscaperTests {} diff --git a/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/JSONEscaper/JSONEscaperTests+BackslashEscaping.swift b/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/JSONEscaper/JSONEscaperTests+BackslashEscaping.swift index e68ef8af..8889a3f3 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/JSONEscaper/JSONEscaperTests+BackslashEscaping.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/JSONEscaper/JSONEscaperTests+BackslashEscaping.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/JSONEscaper/JSONEscaperTests+BackspaceEscaping.swift b/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/JSONEscaper/JSONEscaperTests+BackspaceEscaping.swift index 64f2d514..abc89732 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/JSONEscaper/JSONEscaperTests+BackspaceEscaping.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/JSONEscaper/JSONEscaperTests+BackspaceEscaping.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/JSONEscaper/JSONEscaperTests+CarriageReturnEscaping.swift b/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/JSONEscaper/JSONEscaperTests+CarriageReturnEscaping.swift index ab7f2ae4..a80a7176 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/JSONEscaper/JSONEscaperTests+CarriageReturnEscaping.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/JSONEscaper/JSONEscaperTests+CarriageReturnEscaping.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/JSONEscaper/JSONEscaperTests+Combination.swift b/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/JSONEscaper/JSONEscaperTests+Combination.swift index 08cd42d1..61b53ab6 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/JSONEscaper/JSONEscaperTests+Combination.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/JSONEscaper/JSONEscaperTests+Combination.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/JSONEscaper/JSONEscaperTests+EdgeCases.swift b/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/JSONEscaper/JSONEscaperTests+EdgeCases.swift index dab84391..724d2e8b 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/JSONEscaper/JSONEscaperTests+EdgeCases.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/JSONEscaper/JSONEscaperTests+EdgeCases.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/JSONEscaper/JSONEscaperTests+FormFeedEscaping.swift b/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/JSONEscaper/JSONEscaperTests+FormFeedEscaping.swift index 02f07fc6..60aafac3 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/JSONEscaper/JSONEscaperTests+FormFeedEscaping.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/JSONEscaper/JSONEscaperTests+FormFeedEscaping.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/JSONEscaper/JSONEscaperTests+NewlineEscaping.swift b/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/JSONEscaper/JSONEscaperTests+NewlineEscaping.swift index 3e8e8645..cfa638b5 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/JSONEscaper/JSONEscaperTests+NewlineEscaping.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/JSONEscaper/JSONEscaperTests+NewlineEscaping.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/JSONEscaper/JSONEscaperTests+PlainString.swift b/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/JSONEscaper/JSONEscaperTests+PlainString.swift index 819e3912..0766f298 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/JSONEscaper/JSONEscaperTests+PlainString.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/JSONEscaper/JSONEscaperTests+PlainString.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/JSONEscaper/JSONEscaperTests+QuoteEscaping.swift b/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/JSONEscaper/JSONEscaperTests+QuoteEscaping.swift index 708d0525..4f47caa2 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/JSONEscaper/JSONEscaperTests+QuoteEscaping.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/JSONEscaper/JSONEscaperTests+QuoteEscaping.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/JSONEscaper/JSONEscaperTests+TabEscaping.swift b/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/JSONEscaper/JSONEscaperTests+TabEscaping.swift index 42f86666..ff6f591f 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/JSONEscaper/JSONEscaperTests+TabEscaping.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/JSONEscaper/JSONEscaperTests+TabEscaping.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/JSONEscaper/JSONEscaperTests+UnicodeAndEmoji.swift b/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/JSONEscaper/JSONEscaperTests+UnicodeAndEmoji.swift index 871ed7ed..e28264a1 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/JSONEscaper/JSONEscaperTests+UnicodeAndEmoji.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/JSONEscaper/JSONEscaperTests+UnicodeAndEmoji.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/JSONEscaper/JSONEscaperTests.swift b/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/JSONEscaper/JSONEscaperTests.swift index 45ae5f50..85bf039c 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/JSONEscaper/JSONEscaperTests.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/JSONEscaper/JSONEscaperTests.swift @@ -27,7 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Testing +internal import Testing @Suite("JSONEscaper - JSON String Escaping") internal enum JSONEscaperTests {} diff --git a/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/OutputEscaperFactoryTests.swift b/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/OutputEscaperFactoryTests.swift index 33044de9..8fbfe214 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/OutputEscaperFactoryTests.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/OutputEscaperFactoryTests.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/TableEscaper/TableEscaperTests+CarriageReturnConversion.swift b/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/TableEscaper/TableEscaperTests+CarriageReturnConversion.swift index ff8c0ca3..7105b4ba 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/TableEscaper/TableEscaperTests+CarriageReturnConversion.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/TableEscaper/TableEscaperTests+CarriageReturnConversion.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/TableEscaper/TableEscaperTests+Combination.swift b/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/TableEscaper/TableEscaperTests+Combination.swift index 02df306c..27fc8526 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/TableEscaper/TableEscaperTests+Combination.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/TableEscaper/TableEscaperTests+Combination.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/TableEscaper/TableEscaperTests+EdgeCases.swift b/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/TableEscaper/TableEscaperTests+EdgeCases.swift index 00ef409a..144e8249 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/TableEscaper/TableEscaperTests+EdgeCases.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/TableEscaper/TableEscaperTests+EdgeCases.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/TableEscaper/TableEscaperTests+NewlineConversion.swift b/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/TableEscaper/TableEscaperTests+NewlineConversion.swift index d45ac9b6..7d30d667 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/TableEscaper/TableEscaperTests+NewlineConversion.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/TableEscaper/TableEscaperTests+NewlineConversion.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/TableEscaper/TableEscaperTests+PlainString.swift b/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/TableEscaper/TableEscaperTests+PlainString.swift index e31c7cec..17fb14d8 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/TableEscaper/TableEscaperTests+PlainString.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/TableEscaper/TableEscaperTests+PlainString.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/TableEscaper/TableEscaperTests+TabConversion.swift b/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/TableEscaper/TableEscaperTests+TabConversion.swift index d22f8f63..73d4cc2d 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/TableEscaper/TableEscaperTests+TabConversion.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/TableEscaper/TableEscaperTests+TabConversion.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/TableEscaper/TableEscaperTests+UnicodeAndEmoji.swift b/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/TableEscaper/TableEscaperTests+UnicodeAndEmoji.swift index 5dc343c7..abe7fbdd 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/TableEscaper/TableEscaperTests+UnicodeAndEmoji.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/TableEscaper/TableEscaperTests+UnicodeAndEmoji.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/TableEscaper/TableEscaperTests+WhitespaceTrimming.swift b/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/TableEscaper/TableEscaperTests+WhitespaceTrimming.swift index 420ba7e8..892fb18e 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/TableEscaper/TableEscaperTests+WhitespaceTrimming.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/TableEscaper/TableEscaperTests+WhitespaceTrimming.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/TableEscaper/TableEscaperTests.swift b/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/TableEscaper/TableEscaperTests.swift index b36444d1..a134945a 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/TableEscaper/TableEscaperTests.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/TableEscaper/TableEscaperTests.swift @@ -27,7 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Testing +internal import Testing @Suite("TableEscaper - Single-Line Conversion") internal enum TableEscaperTests {} diff --git a/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/YAMLEscaper/YAMLEscaperTests+BooleanLikeString.swift b/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/YAMLEscaper/YAMLEscaperTests+BooleanLikeString.swift index 2331694c..e0b649b8 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/YAMLEscaper/YAMLEscaperTests+BooleanLikeString.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/YAMLEscaper/YAMLEscaperTests+BooleanLikeString.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/YAMLEscaper/YAMLEscaperTests+ComplexEdgeCases.swift b/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/YAMLEscaper/YAMLEscaperTests+ComplexEdgeCases.swift index d30ccbf5..6ea01e2e 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/YAMLEscaper/YAMLEscaperTests+ComplexEdgeCases.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/YAMLEscaper/YAMLEscaperTests+ComplexEdgeCases.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/YAMLEscaper/YAMLEscaperTests+EmptyString.swift b/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/YAMLEscaper/YAMLEscaperTests+EmptyString.swift index 145cda00..6da2b365 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/YAMLEscaper/YAMLEscaperTests+EmptyString.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/YAMLEscaper/YAMLEscaperTests+EmptyString.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/YAMLEscaper/YAMLEscaperTests+MultiLineString.swift b/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/YAMLEscaper/YAMLEscaperTests+MultiLineString.swift index 08e4fc45..65dc5342 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/YAMLEscaper/YAMLEscaperTests+MultiLineString.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/YAMLEscaper/YAMLEscaperTests+MultiLineString.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/YAMLEscaper/YAMLEscaperTests+NullLikeString.swift b/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/YAMLEscaper/YAMLEscaperTests+NullLikeString.swift index ba4d0eab..462a01e6 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/YAMLEscaper/YAMLEscaperTests+NullLikeString.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/YAMLEscaper/YAMLEscaperTests+NullLikeString.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/YAMLEscaper/YAMLEscaperTests+NumericString.swift b/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/YAMLEscaper/YAMLEscaperTests+NumericString.swift index 4e0dacd0..ba01e7d5 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/YAMLEscaper/YAMLEscaperTests+NumericString.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/YAMLEscaper/YAMLEscaperTests+NumericString.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/YAMLEscaper/YAMLEscaperTests+PlainString.swift b/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/YAMLEscaper/YAMLEscaperTests+PlainString.swift index 981c529b..943e9561 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/YAMLEscaper/YAMLEscaperTests+PlainString.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/YAMLEscaper/YAMLEscaperTests+PlainString.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/YAMLEscaper/YAMLEscaperTests+QuoteAndBackslashEscaping.swift b/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/YAMLEscaper/YAMLEscaperTests+QuoteAndBackslashEscaping.swift index 2f7a4ebb..16657ecf 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/YAMLEscaper/YAMLEscaperTests+QuoteAndBackslashEscaping.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/YAMLEscaper/YAMLEscaperTests+QuoteAndBackslashEscaping.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/YAMLEscaper/YAMLEscaperTests+SpecialCharacter.swift b/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/YAMLEscaper/YAMLEscaperTests+SpecialCharacter.swift index d4efbb2c..d7c82fc2 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/YAMLEscaper/YAMLEscaperTests+SpecialCharacter.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/YAMLEscaper/YAMLEscaperTests+SpecialCharacter.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/YAMLEscaper/YAMLEscaperTests+UnicodeAndEmoji.swift b/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/YAMLEscaper/YAMLEscaperTests+UnicodeAndEmoji.swift index 92c32e7d..e52c8bcf 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/YAMLEscaper/YAMLEscaperTests+UnicodeAndEmoji.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/YAMLEscaper/YAMLEscaperTests+UnicodeAndEmoji.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/YAMLEscaper/YAMLEscaperTests+Whitespace.swift b/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/YAMLEscaper/YAMLEscaperTests+Whitespace.swift index 01346e87..13308762 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/YAMLEscaper/YAMLEscaperTests+Whitespace.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/YAMLEscaper/YAMLEscaperTests+Whitespace.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/YAMLEscaper/YAMLEscaperTests.swift b/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/YAMLEscaper/YAMLEscaperTests.swift index 754b46a8..bbf01f49 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/YAMLEscaper/YAMLEscaperTests.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Output/Escapers/YAMLEscaper/YAMLEscaperTests.swift @@ -27,7 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Testing +internal import Testing @Suite("YAMLEscaper - YAML String Formatting") internal enum YAMLEscaperTests {} diff --git a/Examples/MistDemo/Tests/MistDemoTests/Output/Formatters/CSVFormatter/CSVFormatterTests+CSVEscaping.swift b/Examples/MistDemo/Tests/MistDemoTests/Output/Formatters/CSVFormatter/CSVFormatterTests+CSVEscaping.swift index ffcff412..9ab78a39 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Output/Formatters/CSVFormatter/CSVFormatterTests+CSVEscaping.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Output/Formatters/CSVFormatter/CSVFormatterTests+CSVEscaping.swift @@ -27,9 +27,9 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import MistKit -import Testing +internal import Foundation +internal import MistKit +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Output/Formatters/CSVFormatter/CSVFormatterTests+EdgeCases.swift b/Examples/MistDemo/Tests/MistDemoTests/Output/Formatters/CSVFormatter/CSVFormatterTests+EdgeCases.swift index e45568f1..29303708 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Output/Formatters/CSVFormatter/CSVFormatterTests+EdgeCases.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Output/Formatters/CSVFormatter/CSVFormatterTests+EdgeCases.swift @@ -27,9 +27,9 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import MistKit -import Testing +internal import Foundation +internal import MistKit +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Output/Formatters/CSVFormatter/CSVFormatterTests+RecordInfo.swift b/Examples/MistDemo/Tests/MistDemoTests/Output/Formatters/CSVFormatter/CSVFormatterTests+RecordInfo.swift index 0e198bbd..a7e66fe4 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Output/Formatters/CSVFormatter/CSVFormatterTests+RecordInfo.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Output/Formatters/CSVFormatter/CSVFormatterTests+RecordInfo.swift @@ -27,9 +27,9 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import MistKit -import Testing +internal import Foundation +internal import MistKit +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Output/Formatters/CSVFormatter/CSVFormatterTests+UserInfo.swift b/Examples/MistDemo/Tests/MistDemoTests/Output/Formatters/CSVFormatter/CSVFormatterTests+UserInfo.swift index fa444ded..1d9dbeb3 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Output/Formatters/CSVFormatter/CSVFormatterTests+UserInfo.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Output/Formatters/CSVFormatter/CSVFormatterTests+UserInfo.swift @@ -27,9 +27,9 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import MistKit -import Testing +internal import Foundation +internal import MistKit +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Output/Formatters/CSVFormatter/CSVFormatterTests.swift b/Examples/MistDemo/Tests/MistDemoTests/Output/Formatters/CSVFormatter/CSVFormatterTests.swift index 6c8309f7..7059b212 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Output/Formatters/CSVFormatter/CSVFormatterTests.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Output/Formatters/CSVFormatter/CSVFormatterTests.swift @@ -27,7 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Testing +internal import Testing @Suite("CSVFormatter") internal enum CSVFormatterTests {} diff --git a/Examples/MistDemo/Tests/MistDemoTests/Output/Formatters/OutputFormatterFactory/OutputFormatterFactoryTests+EdgeCases.swift b/Examples/MistDemo/Tests/MistDemoTests/Output/Formatters/OutputFormatterFactory/OutputFormatterFactoryTests+EdgeCases.swift index b4e9641c..6536d7c3 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Output/Formatters/OutputFormatterFactory/OutputFormatterFactoryTests+EdgeCases.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Output/Formatters/OutputFormatterFactory/OutputFormatterFactoryTests+EdgeCases.swift @@ -27,9 +27,9 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import MistKit -import Testing +internal import Foundation +internal import MistKit +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Output/Formatters/OutputFormatterFactory/OutputFormatterFactoryTests+FactoryCreation.swift b/Examples/MistDemo/Tests/MistDemoTests/Output/Formatters/OutputFormatterFactory/OutputFormatterFactoryTests+FactoryCreation.swift index 3a2e2c3f..309265d2 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Output/Formatters/OutputFormatterFactory/OutputFormatterFactoryTests+FactoryCreation.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Output/Formatters/OutputFormatterFactory/OutputFormatterFactoryTests+FactoryCreation.swift @@ -27,9 +27,9 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import MistKit -import Testing +internal import Foundation +internal import MistKit +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Output/Formatters/OutputFormatterFactory/OutputFormatterFactoryTests+FormatSpecificOutput.swift b/Examples/MistDemo/Tests/MistDemoTests/Output/Formatters/OutputFormatterFactory/OutputFormatterFactoryTests+FormatSpecificOutput.swift index b32d218c..f776e8f9 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Output/Formatters/OutputFormatterFactory/OutputFormatterFactoryTests+FormatSpecificOutput.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Output/Formatters/OutputFormatterFactory/OutputFormatterFactoryTests+FormatSpecificOutput.swift @@ -27,9 +27,9 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import MistKit -import Testing +internal import Foundation +internal import MistKit +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Output/Formatters/OutputFormatterFactory/OutputFormatterFactoryTests+FormatterBehaviorConsistency.swift b/Examples/MistDemo/Tests/MistDemoTests/Output/Formatters/OutputFormatterFactory/OutputFormatterFactoryTests+FormatterBehaviorConsistency.swift index 4e391c55..57fb64c3 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Output/Formatters/OutputFormatterFactory/OutputFormatterFactoryTests+FormatterBehaviorConsistency.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Output/Formatters/OutputFormatterFactory/OutputFormatterFactoryTests+FormatterBehaviorConsistency.swift @@ -27,9 +27,9 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import MistKit -import Testing +internal import Foundation +internal import MistKit +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Output/Formatters/OutputFormatterFactory/OutputFormatterFactoryTests+Integration.swift b/Examples/MistDemo/Tests/MistDemoTests/Output/Formatters/OutputFormatterFactory/OutputFormatterFactoryTests+Integration.swift index df200564..13831dbc 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Output/Formatters/OutputFormatterFactory/OutputFormatterFactoryTests+Integration.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Output/Formatters/OutputFormatterFactory/OutputFormatterFactoryTests+Integration.swift @@ -27,9 +27,9 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import MistKit -import Testing +internal import Foundation +internal import MistKit +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Output/Formatters/OutputFormatterFactory/OutputFormatterFactoryTests+OutputFormatEnum.swift b/Examples/MistDemo/Tests/MistDemoTests/Output/Formatters/OutputFormatterFactory/OutputFormatterFactoryTests+OutputFormatEnum.swift index f3c72d3f..1bce592b 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Output/Formatters/OutputFormatterFactory/OutputFormatterFactoryTests+OutputFormatEnum.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Output/Formatters/OutputFormatterFactory/OutputFormatterFactoryTests+OutputFormatEnum.swift @@ -27,9 +27,9 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import MistKit -import Testing +internal import Foundation +internal import MistKit +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Output/Formatters/OutputFormatterFactory/OutputFormatterFactoryTests.swift b/Examples/MistDemo/Tests/MistDemoTests/Output/Formatters/OutputFormatterFactory/OutputFormatterFactoryTests.swift index 5652c981..c7d80938 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Output/Formatters/OutputFormatterFactory/OutputFormatterFactoryTests.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Output/Formatters/OutputFormatterFactory/OutputFormatterFactoryTests.swift @@ -27,7 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Testing +internal import Testing @Suite("OutputFormatterFactory") internal enum OutputFormatterFactoryTests {} diff --git a/Examples/MistDemo/Tests/MistDemoTests/Output/Formatters/TableFormatter/TableFormatterTests+EdgeCases+FieldTypes.swift b/Examples/MistDemo/Tests/MistDemoTests/Output/Formatters/TableFormatter/TableFormatterTests+EdgeCases+FieldTypes.swift index e11cc272..cca9af32 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Output/Formatters/TableFormatter/TableFormatterTests+EdgeCases+FieldTypes.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Output/Formatters/TableFormatter/TableFormatterTests+EdgeCases+FieldTypes.swift @@ -27,9 +27,9 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import MistKit -import Testing +internal import Foundation +internal import MistKit +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Output/Formatters/TableFormatter/TableFormatterTests+EdgeCases+Whitespace.swift b/Examples/MistDemo/Tests/MistDemoTests/Output/Formatters/TableFormatter/TableFormatterTests+EdgeCases+Whitespace.swift index 6fff7c97..f8339f66 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Output/Formatters/TableFormatter/TableFormatterTests+EdgeCases+Whitespace.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Output/Formatters/TableFormatter/TableFormatterTests+EdgeCases+Whitespace.swift @@ -27,9 +27,9 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import MistKit -import Testing +internal import Foundation +internal import MistKit +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Output/Formatters/TableFormatter/TableFormatterTests+EdgeCases.swift b/Examples/MistDemo/Tests/MistDemoTests/Output/Formatters/TableFormatter/TableFormatterTests+EdgeCases.swift index 1d74bb1d..8eb620cb 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Output/Formatters/TableFormatter/TableFormatterTests+EdgeCases.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Output/Formatters/TableFormatter/TableFormatterTests+EdgeCases.swift @@ -27,7 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Testing +internal import Testing extension TableFormatterTests { @Suite("Edge Cases") diff --git a/Examples/MistDemo/Tests/MistDemoTests/Output/Formatters/TableFormatter/TableFormatterTests+RecordInfo.swift b/Examples/MistDemo/Tests/MistDemoTests/Output/Formatters/TableFormatter/TableFormatterTests+RecordInfo.swift index b7615331..efc637e5 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Output/Formatters/TableFormatter/TableFormatterTests+RecordInfo.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Output/Formatters/TableFormatter/TableFormatterTests+RecordInfo.swift @@ -27,9 +27,9 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import MistKit -import Testing +internal import Foundation +internal import MistKit +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Output/Formatters/TableFormatter/TableFormatterTests+SingleLineConversion.swift b/Examples/MistDemo/Tests/MistDemoTests/Output/Formatters/TableFormatter/TableFormatterTests+SingleLineConversion.swift index 8f8b6b04..d98ba75f 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Output/Formatters/TableFormatter/TableFormatterTests+SingleLineConversion.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Output/Formatters/TableFormatter/TableFormatterTests+SingleLineConversion.swift @@ -27,9 +27,9 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import MistKit -import Testing +internal import Foundation +internal import MistKit +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Output/Formatters/TableFormatter/TableFormatterTests+UserInfo.swift b/Examples/MistDemo/Tests/MistDemoTests/Output/Formatters/TableFormatter/TableFormatterTests+UserInfo.swift index d67e68de..9f21f599 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Output/Formatters/TableFormatter/TableFormatterTests+UserInfo.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Output/Formatters/TableFormatter/TableFormatterTests+UserInfo.swift @@ -27,9 +27,9 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import MistKit -import Testing +internal import Foundation +internal import MistKit +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Output/Formatters/TableFormatter/TableFormatterTests.swift b/Examples/MistDemo/Tests/MistDemoTests/Output/Formatters/TableFormatter/TableFormatterTests.swift index 5aedcfc9..738a65fc 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Output/Formatters/TableFormatter/TableFormatterTests.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Output/Formatters/TableFormatter/TableFormatterTests.swift @@ -27,7 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Testing +internal import Testing @Suite("TableFormatter") internal enum TableFormatterTests {} diff --git a/Examples/MistDemo/Tests/MistDemoTests/Output/Formatters/YAMLFormatter/YAMLFormatterTests+EdgeCases.swift b/Examples/MistDemo/Tests/MistDemoTests/Output/Formatters/YAMLFormatter/YAMLFormatterTests+EdgeCases.swift index 6ff6d81c..0b551598 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Output/Formatters/YAMLFormatter/YAMLFormatterTests+EdgeCases.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Output/Formatters/YAMLFormatter/YAMLFormatterTests+EdgeCases.swift @@ -27,9 +27,9 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import MistKit -import Testing +internal import Foundation +internal import MistKit +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Output/Formatters/YAMLFormatter/YAMLFormatterTests+MultilineString.swift b/Examples/MistDemo/Tests/MistDemoTests/Output/Formatters/YAMLFormatter/YAMLFormatterTests+MultilineString.swift index b90c1dc9..372abf8c 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Output/Formatters/YAMLFormatter/YAMLFormatterTests+MultilineString.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Output/Formatters/YAMLFormatter/YAMLFormatterTests+MultilineString.swift @@ -27,9 +27,9 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import MistKit -import Testing +internal import Foundation +internal import MistKit +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Output/Formatters/YAMLFormatter/YAMLFormatterTests+RecordInfo.swift b/Examples/MistDemo/Tests/MistDemoTests/Output/Formatters/YAMLFormatter/YAMLFormatterTests+RecordInfo.swift index 90a9fce2..4f549863 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Output/Formatters/YAMLFormatter/YAMLFormatterTests+RecordInfo.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Output/Formatters/YAMLFormatter/YAMLFormatterTests+RecordInfo.swift @@ -27,9 +27,9 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import MistKit -import Testing +internal import Foundation +internal import MistKit +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Output/Formatters/YAMLFormatter/YAMLFormatterTests+UserInfo.swift b/Examples/MistDemo/Tests/MistDemoTests/Output/Formatters/YAMLFormatter/YAMLFormatterTests+UserInfo.swift index 450ad301..66020b65 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Output/Formatters/YAMLFormatter/YAMLFormatterTests+UserInfo.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Output/Formatters/YAMLFormatter/YAMLFormatterTests+UserInfo.swift @@ -27,9 +27,9 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import MistKit -import Testing +internal import Foundation +internal import MistKit +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Output/Formatters/YAMLFormatter/YAMLFormatterTests+YAMLEscaping+ReservedStrings.swift b/Examples/MistDemo/Tests/MistDemoTests/Output/Formatters/YAMLFormatter/YAMLFormatterTests+YAMLEscaping+ReservedStrings.swift index 49b8aae5..1a37039b 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Output/Formatters/YAMLFormatter/YAMLFormatterTests+YAMLEscaping+ReservedStrings.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Output/Formatters/YAMLFormatter/YAMLFormatterTests+YAMLEscaping+ReservedStrings.swift @@ -27,9 +27,9 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import MistKit -import Testing +internal import Foundation +internal import MistKit +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Output/Formatters/YAMLFormatter/YAMLFormatterTests+YAMLEscaping+SpecialChars.swift b/Examples/MistDemo/Tests/MistDemoTests/Output/Formatters/YAMLFormatter/YAMLFormatterTests+YAMLEscaping+SpecialChars.swift index b53531e4..3a088193 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Output/Formatters/YAMLFormatter/YAMLFormatterTests+YAMLEscaping+SpecialChars.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Output/Formatters/YAMLFormatter/YAMLFormatterTests+YAMLEscaping+SpecialChars.swift @@ -27,9 +27,9 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import MistKit -import Testing +internal import Foundation +internal import MistKit +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Output/Formatters/YAMLFormatter/YAMLFormatterTests+YAMLEscaping.swift b/Examples/MistDemo/Tests/MistDemoTests/Output/Formatters/YAMLFormatter/YAMLFormatterTests+YAMLEscaping.swift index 5df6d6e7..f2cae3a8 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Output/Formatters/YAMLFormatter/YAMLFormatterTests+YAMLEscaping.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Output/Formatters/YAMLFormatter/YAMLFormatterTests+YAMLEscaping.swift @@ -27,7 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Testing +internal import Testing extension YAMLFormatterTests { @Suite("YAML Escaping") diff --git a/Examples/MistDemo/Tests/MistDemoTests/Output/Formatters/YAMLFormatter/YAMLFormatterTests.swift b/Examples/MistDemo/Tests/MistDemoTests/Output/Formatters/YAMLFormatter/YAMLFormatterTests.swift index bd33b100..7ac69c00 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Output/Formatters/YAMLFormatter/YAMLFormatterTests.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Output/Formatters/YAMLFormatter/YAMLFormatterTests.swift @@ -27,7 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Testing +internal import Testing @Suite("YAMLFormatter") internal enum YAMLFormatterTests {} diff --git a/Examples/MistDemo/Tests/MistDemoTests/Output/JSONFormatterTests.swift b/Examples/MistDemo/Tests/MistDemoTests/Output/JSONFormatterTests.swift index 312601ee..9484c6ca 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Output/JSONFormatterTests.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Output/JSONFormatterTests.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Server/MockBackend.swift b/Examples/MistDemo/Tests/MistDemoTests/Server/MockBackend.swift index d7dc640a..ccc3f6e8 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Server/MockBackend.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Server/MockBackend.swift @@ -28,8 +28,8 @@ // #if canImport(Hummingbird) - import Foundation - import MistKit + internal import Foundation + internal import MistKit @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Server/WebAuthTokenStoreTests.swift b/Examples/MistDemo/Tests/MistDemoTests/Server/WebAuthTokenStoreTests.swift index c83dca9e..65e5c843 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Server/WebAuthTokenStoreTests.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Server/WebAuthTokenStoreTests.swift @@ -28,7 +28,7 @@ // #if canImport(Hummingbird) - import Testing + internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Server/WebJSONTests.swift b/Examples/MistDemo/Tests/MistDemoTests/Server/WebJSONTests.swift index aa90f059..693d3600 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Server/WebJSONTests.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Server/WebJSONTests.swift @@ -28,8 +28,8 @@ // #if canImport(Hummingbird) - import Foundation - import Testing + internal import Foundation + internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Server/WebServerTests+CRUD.swift b/Examples/MistDemo/Tests/MistDemoTests/Server/WebServerTests+CRUD.swift index 28afc0f9..22c0097e 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Server/WebServerTests+CRUD.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Server/WebServerTests+CRUD.swift @@ -28,12 +28,12 @@ // #if canImport(Hummingbird) - import Foundation - import HTTPTypes - import Hummingbird - import HummingbirdTesting - import MistKit - import Testing + internal import Foundation + internal import HTTPTypes + internal import Hummingbird + internal import HummingbirdTesting + internal import MistKit + internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Server/WebServerTests+Database.swift b/Examples/MistDemo/Tests/MistDemoTests/Server/WebServerTests+Database.swift index d1a7106f..dfeaf071 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Server/WebServerTests+Database.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Server/WebServerTests+Database.swift @@ -28,12 +28,12 @@ // #if canImport(Hummingbird) - import Foundation - import HTTPTypes - import Hummingbird - import HummingbirdTesting - import MistKit - import Testing + internal import Foundation + internal import HTTPTypes + internal import Hummingbird + internal import HummingbirdTesting + internal import MistKit + internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Server/WebServerTests+Index.swift b/Examples/MistDemo/Tests/MistDemoTests/Server/WebServerTests+Index.swift index c58a5c22..b2ea234e 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Server/WebServerTests+Index.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Server/WebServerTests+Index.swift @@ -28,12 +28,12 @@ // #if canImport(Hummingbird) - import Foundation - import HTTPTypes - import Hummingbird - import HummingbirdTesting - import MistKit - import Testing + internal import Foundation + internal import HTTPTypes + internal import Hummingbird + internal import HummingbirdTesting + internal import MistKit + internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Server/WebServerTests+QuerySort.swift b/Examples/MistDemo/Tests/MistDemoTests/Server/WebServerTests+QuerySort.swift index 8db89013..5d0ab6c2 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Server/WebServerTests+QuerySort.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Server/WebServerTests+QuerySort.swift @@ -28,12 +28,12 @@ // #if canImport(Hummingbird) - import Foundation - import HTTPTypes - import Hummingbird - import HummingbirdTesting - import MistKit - import Testing + internal import Foundation + internal import HTTPTypes + internal import Hummingbird + internal import HummingbirdTesting + internal import MistKit + internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Server/WebServerTests.swift b/Examples/MistDemo/Tests/MistDemoTests/Server/WebServerTests.swift index 4d1b1bee..cf8cc456 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Server/WebServerTests.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Server/WebServerTests.swift @@ -28,12 +28,12 @@ // #if canImport(Hummingbird) - import Foundation - import HTTPTypes - import Hummingbird - import HummingbirdTesting - import MistKit - import Testing + internal import Foundation + internal import HTTPTypes + internal import Hummingbird + internal import HummingbirdTesting + internal import MistKit + internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Types/AnyCodable/AnyCodableTests+BooleanDecoding.swift b/Examples/MistDemo/Tests/MistDemoTests/Types/AnyCodable/AnyCodableTests+BooleanDecoding.swift index 484c2ee8..e451a450 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Types/AnyCodable/AnyCodableTests+BooleanDecoding.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Types/AnyCodable/AnyCodableTests+BooleanDecoding.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Types/AnyCodable/AnyCodableTests+DoubleDecoding.swift b/Examples/MistDemo/Tests/MistDemoTests/Types/AnyCodable/AnyCodableTests+DoubleDecoding.swift index 491c586f..e879f071 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Types/AnyCodable/AnyCodableTests+DoubleDecoding.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Types/AnyCodable/AnyCodableTests+DoubleDecoding.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Types/AnyCodable/AnyCodableTests+Encoding.swift b/Examples/MistDemo/Tests/MistDemoTests/Types/AnyCodable/AnyCodableTests+Encoding.swift index 15257a0f..70952eac 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Types/AnyCodable/AnyCodableTests+Encoding.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Types/AnyCodable/AnyCodableTests+Encoding.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Types/AnyCodable/AnyCodableTests+Errors.swift b/Examples/MistDemo/Tests/MistDemoTests/Types/AnyCodable/AnyCodableTests+Errors.swift index 34a594ba..6f96bf20 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Types/AnyCodable/AnyCodableTests+Errors.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Types/AnyCodable/AnyCodableTests+Errors.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Types/AnyCodable/AnyCodableTests+IntegerDecoding.swift b/Examples/MistDemo/Tests/MistDemoTests/Types/AnyCodable/AnyCodableTests+IntegerDecoding.swift index 943f51a6..3fc6c404 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Types/AnyCodable/AnyCodableTests+IntegerDecoding.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Types/AnyCodable/AnyCodableTests+IntegerDecoding.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Types/AnyCodable/AnyCodableTests+NullDecoding.swift b/Examples/MistDemo/Tests/MistDemoTests/Types/AnyCodable/AnyCodableTests+NullDecoding.swift index 339b2984..c0397f6d 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Types/AnyCodable/AnyCodableTests+NullDecoding.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Types/AnyCodable/AnyCodableTests+NullDecoding.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Types/AnyCodable/AnyCodableTests+RoundTrip.swift b/Examples/MistDemo/Tests/MistDemoTests/Types/AnyCodable/AnyCodableTests+RoundTrip.swift index 8bc63c8f..761f6bef 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Types/AnyCodable/AnyCodableTests+RoundTrip.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Types/AnyCodable/AnyCodableTests+RoundTrip.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Types/AnyCodable/AnyCodableTests+StringDecoding.swift b/Examples/MistDemo/Tests/MistDemoTests/Types/AnyCodable/AnyCodableTests+StringDecoding.swift index bda3eae4..d0993e6b 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Types/AnyCodable/AnyCodableTests+StringDecoding.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Types/AnyCodable/AnyCodableTests+StringDecoding.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Types/AnyCodable/AnyCodableTests.swift b/Examples/MistDemo/Tests/MistDemoTests/Types/AnyCodable/AnyCodableTests.swift index 1f38ed80..dc93cce5 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Types/AnyCodable/AnyCodableTests.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Types/AnyCodable/AnyCodableTests.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Types/DynamicKey/DynamicKeyTests+CodingKeyConformance.swift b/Examples/MistDemo/Tests/MistDemoTests/Types/DynamicKey/DynamicKeyTests+CodingKeyConformance.swift index ab9271a3..1d34436d 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Types/DynamicKey/DynamicKeyTests+CodingKeyConformance.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Types/DynamicKey/DynamicKeyTests+CodingKeyConformance.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Types/DynamicKey/DynamicKeyTests+Equality.swift b/Examples/MistDemo/Tests/MistDemoTests/Types/DynamicKey/DynamicKeyTests+Equality.swift index 0790cc3c..a09c554e 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Types/DynamicKey/DynamicKeyTests+Equality.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Types/DynamicKey/DynamicKeyTests+Equality.swift @@ -27,7 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Testing +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Types/DynamicKey/DynamicKeyTests+IntegerInitialization.swift b/Examples/MistDemo/Tests/MistDemoTests/Types/DynamicKey/DynamicKeyTests+IntegerInitialization.swift index 0eaf2bdc..ea092ff4 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Types/DynamicKey/DynamicKeyTests+IntegerInitialization.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Types/DynamicKey/DynamicKeyTests+IntegerInitialization.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Types/DynamicKey/DynamicKeyTests+StringInitialization.swift b/Examples/MistDemo/Tests/MistDemoTests/Types/DynamicKey/DynamicKeyTests+StringInitialization.swift index 933bfac0..473e8f49 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Types/DynamicKey/DynamicKeyTests+StringInitialization.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Types/DynamicKey/DynamicKeyTests+StringInitialization.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Types/DynamicKey/DynamicKeyTests.swift b/Examples/MistDemo/Tests/MistDemoTests/Types/DynamicKey/DynamicKeyTests.swift index de4b509a..b187f344 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Types/DynamicKey/DynamicKeyTests.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Types/DynamicKey/DynamicKeyTests.swift @@ -27,7 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Testing +internal import Testing @Suite("DynamicKey") internal enum DynamicKeyTests {} diff --git a/Examples/MistDemo/Tests/MistDemoTests/Types/FieldInputValue/FieldInputValueTests+BoolCase.swift b/Examples/MistDemo/Tests/MistDemoTests/Types/FieldInputValue/FieldInputValueTests+BoolCase.swift index c74e9c57..10d5c4db 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Types/FieldInputValue/FieldInputValueTests+BoolCase.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Types/FieldInputValue/FieldInputValueTests+BoolCase.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Types/FieldInputValue/FieldInputValueTests+DoubleCase.swift b/Examples/MistDemo/Tests/MistDemoTests/Types/FieldInputValue/FieldInputValueTests+DoubleCase.swift index 0ac19da6..2d339a52 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Types/FieldInputValue/FieldInputValueTests+DoubleCase.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Types/FieldInputValue/FieldInputValueTests+DoubleCase.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Types/FieldInputValue/FieldInputValueTests+EdgeCases.swift b/Examples/MistDemo/Tests/MistDemoTests/Types/FieldInputValue/FieldInputValueTests+EdgeCases.swift index 77c9ddff..2a9b0e24 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Types/FieldInputValue/FieldInputValueTests+EdgeCases.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Types/FieldInputValue/FieldInputValueTests+EdgeCases.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Types/FieldInputValue/FieldInputValueTests+IntCase.swift b/Examples/MistDemo/Tests/MistDemoTests/Types/FieldInputValue/FieldInputValueTests+IntCase.swift index 8e7762b7..8ea2ca3c 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Types/FieldInputValue/FieldInputValueTests+IntCase.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Types/FieldInputValue/FieldInputValueTests+IntCase.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Types/FieldInputValue/FieldInputValueTests+StringCase.swift b/Examples/MistDemo/Tests/MistDemoTests/Types/FieldInputValue/FieldInputValueTests+StringCase.swift index 79395093..e92364f2 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Types/FieldInputValue/FieldInputValueTests+StringCase.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Types/FieldInputValue/FieldInputValueTests+StringCase.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Types/FieldInputValue/FieldInputValueTests.swift b/Examples/MistDemo/Tests/MistDemoTests/Types/FieldInputValue/FieldInputValueTests.swift index a9a8b9a8..c827e207 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Types/FieldInputValue/FieldInputValueTests.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Types/FieldInputValue/FieldInputValueTests.swift @@ -27,7 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Testing +internal import Testing @Suite("FieldInputValue Conversion") internal enum FieldInputValueTests {} diff --git a/Examples/MistDemo/Tests/MistDemoTests/Types/FieldsInput/FieldsInputTests+BooleanFieldDecoding.swift b/Examples/MistDemo/Tests/MistDemoTests/Types/FieldsInput/FieldsInputTests+BooleanFieldDecoding.swift index 66c4f6ed..169f77b0 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Types/FieldsInput/FieldsInputTests+BooleanFieldDecoding.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Types/FieldsInput/FieldsInputTests+BooleanFieldDecoding.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Types/FieldsInput/FieldsInputTests+DoubleFieldDecoding.swift b/Examples/MistDemo/Tests/MistDemoTests/Types/FieldsInput/FieldsInputTests+DoubleFieldDecoding.swift index 43d7dbb4..0faa26f2 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Types/FieldsInput/FieldsInputTests+DoubleFieldDecoding.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Types/FieldsInput/FieldsInputTests+DoubleFieldDecoding.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Types/FieldsInput/FieldsInputTests+Encoding.swift b/Examples/MistDemo/Tests/MistDemoTests/Types/FieldsInput/FieldsInputTests+Encoding.swift index fde65ea8..347d32d9 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Types/FieldsInput/FieldsInputTests+Encoding.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Types/FieldsInput/FieldsInputTests+Encoding.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Types/FieldsInput/FieldsInputTests+FieldName.swift b/Examples/MistDemo/Tests/MistDemoTests/Types/FieldsInput/FieldsInputTests+FieldName.swift index 0c4a8fa8..b12dff3d 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Types/FieldsInput/FieldsInputTests+FieldName.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Types/FieldsInput/FieldsInputTests+FieldName.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Types/FieldsInput/FieldsInputTests+IntegerFieldDecoding.swift b/Examples/MistDemo/Tests/MistDemoTests/Types/FieldsInput/FieldsInputTests+IntegerFieldDecoding.swift index c582c7d3..7645a7fd 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Types/FieldsInput/FieldsInputTests+IntegerFieldDecoding.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Types/FieldsInput/FieldsInputTests+IntegerFieldDecoding.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Types/FieldsInput/FieldsInputTests+MultipleFields.swift b/Examples/MistDemo/Tests/MistDemoTests/Types/FieldsInput/FieldsInputTests+MultipleFields.swift index 817d1a94..97ca5804 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Types/FieldsInput/FieldsInputTests+MultipleFields.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Types/FieldsInput/FieldsInputTests+MultipleFields.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Types/FieldsInput/FieldsInputTests+SpecialValue.swift b/Examples/MistDemo/Tests/MistDemoTests/Types/FieldsInput/FieldsInputTests+SpecialValue.swift index 7a8bafe4..2cc12379 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Types/FieldsInput/FieldsInputTests+SpecialValue.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Types/FieldsInput/FieldsInputTests+SpecialValue.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Types/FieldsInput/FieldsInputTests+StringFieldDecoding.swift b/Examples/MistDemo/Tests/MistDemoTests/Types/FieldsInput/FieldsInputTests+StringFieldDecoding.swift index 2375611b..ddd29069 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Types/FieldsInput/FieldsInputTests+StringFieldDecoding.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Types/FieldsInput/FieldsInputTests+StringFieldDecoding.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Types/FieldsInput/FieldsInputTests.swift b/Examples/MistDemo/Tests/MistDemoTests/Types/FieldsInput/FieldsInputTests.swift index 4a17f5b9..4c30758d 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Types/FieldsInput/FieldsInputTests.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Types/FieldsInput/FieldsInputTests.swift @@ -27,7 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Testing +internal import Testing @Suite("FieldsInput") internal enum FieldsInputTests {} diff --git a/Examples/MistDemo/Tests/MistDemoTests/UserInfoTestExtension.swift b/Examples/MistDemo/Tests/MistDemoTests/UserInfoTestExtension.swift index f04be53c..7a2e388e 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/UserInfoTestExtension.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/UserInfoTestExtension.swift @@ -27,7 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation +internal import Foundation internal import MistKitOpenAPI @testable import MistKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Utilities/AsyncHelpers/AsyncHelpersTests+AsyncTimeoutError.swift b/Examples/MistDemo/Tests/MistDemoTests/Utilities/AsyncHelpers/AsyncHelpersTests+AsyncTimeoutError.swift index b41e5bb9..d6b07076 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Utilities/AsyncHelpers/AsyncHelpersTests+AsyncTimeoutError.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Utilities/AsyncHelpers/AsyncHelpersTests+AsyncTimeoutError.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Utilities/AsyncHelpers/AsyncHelpersTests+ConcurrentTimeout.swift b/Examples/MistDemo/Tests/MistDemoTests/Utilities/AsyncHelpers/AsyncHelpersTests+ConcurrentTimeout.swift index c54a7742..e2059c75 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Utilities/AsyncHelpers/AsyncHelpersTests+ConcurrentTimeout.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Utilities/AsyncHelpers/AsyncHelpersTests+ConcurrentTimeout.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistDemoKit @@ -43,12 +43,13 @@ extension AsyncHelpersTests { ) ) internal func cancelsOtherTasks() async throws { - // Intermittent on simulator cooperative executors (watchOS in particular): - // the operation's single long Task.sleep can complete before the polling - // timeout's many short sleeps detect the deadline — same root cause as - // the wasm32 gate above and the throwsOnTimeout / returnsAsyncValue - // tests in AsyncHelpersTests+Timeout.swift. - await withKnownIssue(isIntermittent: true) { + // Intermittent only on `TestPlatform.isFlakyTimeoutSimulator` (CI sim + // cooperative executors) — same root cause as the timeout tests in + // AsyncHelpersTests+Timeout.swift; strict everywhere else. See #334. + await withKnownIssue( + isIntermittent: true, + when: TestPlatform.isFlakyTimeoutSimulator + ) { await #expect(throws: AsyncTimeoutError.self) { try await withTimeout(seconds: 0.1) { try await Task.sleep(nanoseconds: 500_000_000) @@ -78,10 +79,12 @@ extension AsyncHelpersTests { } group.addTask { - // Intermittent on watchOS simulator cooperative executor — same root - // cause as `cancelsOtherTasks` above: a single long Task.sleep can win - // the race against the polling timeout's short sleeps. - await withKnownIssue(isIntermittent: true) { + // Intermittent only on `TestPlatform.isFlakyTimeoutSimulator` — same + // root cause as `cancelsOtherTasks` above; strict elsewhere. See #334. + await withKnownIssue( + isIntermittent: true, + when: TestPlatform.isFlakyTimeoutSimulator + ) { do { _ = try await withTimeout(seconds: 0.2) { try await Task.sleep(nanoseconds: 2_000_000_000) diff --git a/Examples/MistDemo/Tests/MistDemoTests/Utilities/AsyncHelpers/AsyncHelpersTests+EdgeCases.swift b/Examples/MistDemo/Tests/MistDemoTests/Utilities/AsyncHelpers/AsyncHelpersTests+EdgeCases.swift index af54aac9..3c8f8bf5 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Utilities/AsyncHelpers/AsyncHelpersTests+EdgeCases.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Utilities/AsyncHelpers/AsyncHelpersTests+EdgeCases.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Utilities/AsyncHelpers/AsyncHelpersTests+FormatTimeout.swift b/Examples/MistDemo/Tests/MistDemoTests/Utilities/AsyncHelpers/AsyncHelpersTests+FormatTimeout.swift index 3ec35b3a..2858d93d 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Utilities/AsyncHelpers/AsyncHelpersTests+FormatTimeout.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Utilities/AsyncHelpers/AsyncHelpersTests+FormatTimeout.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Utilities/AsyncHelpers/AsyncHelpersTests+Timeout.swift b/Examples/MistDemo/Tests/MistDemoTests/Utilities/AsyncHelpers/AsyncHelpersTests+Timeout.swift index 86e8f5bc..e936ffd4 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Utilities/AsyncHelpers/AsyncHelpersTests+Timeout.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Utilities/AsyncHelpers/AsyncHelpersTests+Timeout.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistDemoKit @@ -52,10 +52,13 @@ extension AsyncHelpersTests { ) ) internal func throwsOnTimeout() async { - // Intermittent: simulator cooperative executors (notably watchOS) can let the - // operation's single long Task.sleep complete before the polling timeout task's - // many short sleeps detect the deadline — same root cause as the wasm32 gate. - await withKnownIssue(isIntermittent: true) { + // Intermittent only on `TestPlatform.isFlakyTimeoutSimulator` (visionOS / + // watchOS sim under CI) — same root cause as the wasm32 gate; strict + // everywhere else so a regression in the helper fails loudly. See #334. + await withKnownIssue( + isIntermittent: true, + when: TestPlatform.isFlakyTimeoutSimulator + ) { await #expect(throws: AsyncTimeoutError.self) { try await withTimeout(seconds: 0.1) { try await Task.sleep(nanoseconds: 500_000_000) // 500ms @@ -74,13 +77,15 @@ extension AsyncHelpersTests { ) internal func returnsAsyncValue() async { // The 30 s budget (vs. the operation's 50 ms inner sleep) is intentionally - // generous: under iOS-simulator CI load the operation task's single long + // generous: under CI simulator load the operation task's single long // Task.sleep can be scheduled behind the polling timeout task's many short - // sleeps, so a tighter budget produced flaky timeouts (#283). - // Even at 30s the iOS simulator under heavy CI load can exceed the budget - // (observed wall times of 48-50s for ostensibly trivial operations), so - // mark as intermittent rather than chasing the budget upward indefinitely. - await withKnownIssue(isIntermittent: true) { + // sleeps, so a tighter budget produced flaky timeouts (#283). Intermittent + // only on `TestPlatform.isFlakyTimeoutSimulator` — strict everywhere else + // so a regression in the helper fails loudly. See #334. + await withKnownIssue( + isIntermittent: true, + when: TestPlatform.isFlakyTimeoutSimulator + ) { let result = try await withTimeout(seconds: 30.0) { try await Task.sleep(nanoseconds: 50_000_000) // 50ms return 42 @@ -109,11 +114,15 @@ extension AsyncHelpersTests { ) ) internal func veryShortTimeout() async { - // Same root cause as `throwsOnTimeout` / `returnsAsyncValue`: under - // simulator load (observed on visionOS, run #25990091951) the - // operation's single 100ms Task.sleep can finish before the polling - // timeout task's many short sleeps detect the 1ms deadline. - await withKnownIssue(isIntermittent: true) { + // Same root cause as `throwsOnTimeout` / `returnsAsyncValue`: under CI + // simulator load (observed on visionOS, run #25990091951) the operation's + // single 100ms Task.sleep can finish before the polling timeout task's + // many short sleeps detect the 1ms deadline. Intermittent only on + // `TestPlatform.isFlakyTimeoutSimulator`; strict elsewhere. See #334. + await withKnownIssue( + isIntermittent: true, + when: TestPlatform.isFlakyTimeoutSimulator + ) { await #expect(throws: AsyncTimeoutError.self) { try await withTimeout(seconds: 0.001) { try await Task.sleep(nanoseconds: 100_000_000) // 100ms diff --git a/Examples/MistDemo/Tests/MistDemoTests/Utilities/AsyncHelpers/AsyncHelpersTests.swift b/Examples/MistDemo/Tests/MistDemoTests/Utilities/AsyncHelpers/AsyncHelpersTests.swift index 1d4d2782..50bf06de 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Utilities/AsyncHelpers/AsyncHelpersTests.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Utilities/AsyncHelpers/AsyncHelpersTests.swift @@ -27,7 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Testing +internal import Testing @Suite("AsyncHelpers") internal enum AsyncHelpersTests {} diff --git a/Examples/MistDemo/Tests/MistDemoTests/Utilities/AuthenticationHelper/AuthenticationHelperTests+APIOnlyAuthentication.swift b/Examples/MistDemo/Tests/MistDemoTests/Utilities/AuthenticationHelper/AuthenticationHelperTests+APIOnlyAuthentication.swift index efc186be..b7566f41 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Utilities/AuthenticationHelper/AuthenticationHelperTests+APIOnlyAuthentication.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Utilities/AuthenticationHelper/AuthenticationHelperTests+APIOnlyAuthentication.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistDemoKit @testable import MistKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Utilities/AuthenticationHelper/AuthenticationHelperTests+AuthenticationMethodPriority.swift b/Examples/MistDemo/Tests/MistDemoTests/Utilities/AuthenticationHelper/AuthenticationHelperTests+AuthenticationMethodPriority.swift index a2e92d0f..7232b616 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Utilities/AuthenticationHelper/AuthenticationHelperTests+AuthenticationMethodPriority.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Utilities/AuthenticationHelper/AuthenticationHelperTests+AuthenticationMethodPriority.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistDemoKit @testable import MistKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Utilities/AuthenticationHelper/AuthenticationHelperTests+ServerToServerAuthentication.swift b/Examples/MistDemo/Tests/MistDemoTests/Utilities/AuthenticationHelper/AuthenticationHelperTests+ServerToServerAuthentication.swift index 16c50fa4..b735b65c 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Utilities/AuthenticationHelper/AuthenticationHelperTests+ServerToServerAuthentication.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Utilities/AuthenticationHelper/AuthenticationHelperTests+ServerToServerAuthentication.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistDemoKit @testable import MistKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Utilities/AuthenticationHelper/AuthenticationHelperTests+TokenResolution.swift b/Examples/MistDemo/Tests/MistDemoTests/Utilities/AuthenticationHelper/AuthenticationHelperTests+TokenResolution.swift index 01ac2457..24199081 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Utilities/AuthenticationHelper/AuthenticationHelperTests+TokenResolution.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Utilities/AuthenticationHelper/AuthenticationHelperTests+TokenResolution.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistDemoKit @testable import MistKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Utilities/AuthenticationHelper/AuthenticationHelperTests+WebAuthentication.swift b/Examples/MistDemo/Tests/MistDemoTests/Utilities/AuthenticationHelper/AuthenticationHelperTests+WebAuthentication.swift index 1379cb67..c0e24e6f 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Utilities/AuthenticationHelper/AuthenticationHelperTests+WebAuthentication.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Utilities/AuthenticationHelper/AuthenticationHelperTests+WebAuthentication.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistDemoKit @testable import MistKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Utilities/AuthenticationHelper/AuthenticationHelperTests.swift b/Examples/MistDemo/Tests/MistDemoTests/Utilities/AuthenticationHelper/AuthenticationHelperTests.swift index e4a71a57..2f381186 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Utilities/AuthenticationHelper/AuthenticationHelperTests.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Utilities/AuthenticationHelper/AuthenticationHelperTests.swift @@ -27,7 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Testing +internal import Testing @Suite("AuthenticationHelper") internal enum AuthenticationHelperTests { diff --git a/Examples/MistDemo/Tests/MistDemoTests/Utilities/EnvironmentTraits.swift b/Examples/MistDemo/Tests/MistDemoTests/Utilities/EnvironmentTraits.swift index e1f72e64..58bc0d73 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Utilities/EnvironmentTraits.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Utilities/EnvironmentTraits.swift @@ -27,7 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Testing +internal import Testing /// A `TestTrait` / `SuiteTrait` that scopes a fake environment for the test. /// Apply with `.mockEnvironment(["KEY": "value"])` to declare the environment diff --git a/Examples/MistDemo/Tests/MistDemoTests/Utilities/LoopbackAuthorityTests.swift b/Examples/MistDemo/Tests/MistDemoTests/Utilities/LoopbackAuthorityTests.swift index ecabbaab..efe4d044 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Utilities/LoopbackAuthorityTests.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Utilities/LoopbackAuthorityTests.swift @@ -27,7 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Testing +internal import Testing @testable import MistDemoKit diff --git a/Examples/MistDemo/Tests/MistDemoTests/Utilities/TestPlatform.swift b/Examples/MistDemo/Tests/MistDemoTests/Utilities/TestPlatform.swift index 62598d61..209dc43d 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Utilities/TestPlatform.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Utilities/TestPlatform.swift @@ -27,6 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // +internal import Foundation + /// Compile-time platform constants exposed as runtime values so tests can read /// them via Swift Testing traits like `.enabled(if:)` / `.disabled(if:)` — /// keeping the gating in a trait on the test rather than `#if` around it. @@ -41,4 +43,16 @@ internal enum TestPlatform { return false #endif }() + + /// True when running inside CI on a simulator whose cooperative executor can + /// starve the polling timeout task in `withTimeout` — see #334. The race is + /// bounded to visionOS / watchOS simulators under CI load; local sim runs + /// (CI env unset) stay strict to surface real regressions in the helper. + internal static let isFlakyTimeoutSimulator: Bool = { + #if (os(visionOS) || os(watchOS)) && targetEnvironment(simulator) + return ProcessInfo.processInfo.environment["CI"] != nil + #else + return false + #endif + }() } diff --git a/Examples/MistDemo/Tests/MistDemoTests/Utilities/WithKnownIssueWhen.swift b/Examples/MistDemo/Tests/MistDemoTests/Utilities/WithKnownIssueWhen.swift new file mode 100644 index 00000000..c11f10a5 --- /dev/null +++ b/Examples/MistDemo/Tests/MistDemoTests/Utilities/WithKnownIssueWhen.swift @@ -0,0 +1,61 @@ +// +// WithKnownIssueWhen.swift +// MistDemo +// +// Created by Leo Dion. +// Copyright © 2026 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +internal import Testing + +/// `withKnownIssue` that only wraps `body` when `when` is true; otherwise runs +/// `body` strictly (any throw is recorded as a real `Issue`). Lets call sites +/// say "expected to flake on these platforms / environments, must succeed +/// everywhere else" without duplicating the body across an if/else. +/// +/// See `TestPlatform.isFlakyTimeoutSimulator` for the canonical caller — gates +/// the `withTimeout` polling-race flake on CI simulator runs (#334) while +/// keeping the assertions strict on every other platform/environment. +internal func withKnownIssue( + _ comment: Comment? = nil, + isIntermittent: Bool = false, + when condition: Bool, + sourceLocation: SourceLocation = #_sourceLocation, + _ body: () async throws -> Void +) async { + if condition { + await withKnownIssue( + comment, + isIntermittent: isIntermittent, + sourceLocation: sourceLocation, + body + ) + } else { + do { + try await body() + } catch { + Issue.record(error, sourceLocation: sourceLocation) + } + } +} diff --git a/Sources/MistKit/Authentication/APITokenManager.swift b/Sources/MistKit/Authentication/APITokenManager.swift index 4660c044..e050df95 100644 --- a/Sources/MistKit/Authentication/APITokenManager.swift +++ b/Sources/MistKit/Authentication/APITokenManager.swift @@ -27,7 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation +internal import Foundation /// Token manager for simple API token authentication. /// Provides container-level access to CloudKit Web Services. diff --git a/Sources/MistKit/Authentication/AdaptiveTokenManager.swift b/Sources/MistKit/Authentication/AdaptiveTokenManager.swift index 6a6e5311..e06ebae9 100644 --- a/Sources/MistKit/Authentication/AdaptiveTokenManager.swift +++ b/Sources/MistKit/Authentication/AdaptiveTokenManager.swift @@ -27,7 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation +internal import Foundation /// Adaptive token manager that can transition between API-only and Web authentication. /// diff --git a/Sources/MistKit/Authentication/AuthenticationMiddleware.swift b/Sources/MistKit/Authentication/AuthenticationMiddleware.swift index aae54ab6..3ffe8401 100644 --- a/Sources/MistKit/Authentication/AuthenticationMiddleware.swift +++ b/Sources/MistKit/Authentication/AuthenticationMiddleware.swift @@ -27,9 +27,9 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import HTTPTypes -import OpenAPIRuntime +internal import Foundation +internal import HTTPTypes +internal import OpenAPIRuntime /// Authentication middleware that delegates request mutation to whichever /// `Authenticator` the `TokenManager` currently vends. diff --git a/Sources/MistKit/Authentication/HTTPField.Name+CloudKit.swift b/Sources/MistKit/Authentication/HTTPField.Name+CloudKit.swift index 124ed774..912e4639 100644 --- a/Sources/MistKit/Authentication/HTTPField.Name+CloudKit.swift +++ b/Sources/MistKit/Authentication/HTTPField.Name+CloudKit.swift @@ -27,7 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import HTTPTypes +internal import HTTPTypes // swiftlint:disable force_unwrapping // swift-format-ignore: NeverForceUnwrap diff --git a/Sources/MistKit/Authentication/InternalErrorReason.swift b/Sources/MistKit/Authentication/InternalErrorReason.swift index 5a020c45..be5e8ed4 100644 --- a/Sources/MistKit/Authentication/InternalErrorReason.swift +++ b/Sources/MistKit/Authentication/InternalErrorReason.swift @@ -27,7 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation +internal import Foundation /// Specific reasons for internal errors public enum InternalErrorReason: Sendable { diff --git a/Sources/MistKit/Authentication/NetworkErrorReason.swift b/Sources/MistKit/Authentication/NetworkErrorReason.swift index 7b706a38..19a58dc5 100644 --- a/Sources/MistKit/Authentication/NetworkErrorReason.swift +++ b/Sources/MistKit/Authentication/NetworkErrorReason.swift @@ -30,7 +30,7 @@ public import Foundation #if canImport(FoundationNetworking) - import FoundationNetworking + internal import FoundationNetworking #endif /// Specific reasons for network errors during authentication diff --git a/Sources/MistKit/Authentication/TokenManager.swift b/Sources/MistKit/Authentication/TokenManager.swift index 6c188ecb..06713352 100644 --- a/Sources/MistKit/Authentication/TokenManager.swift +++ b/Sources/MistKit/Authentication/TokenManager.swift @@ -27,7 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation +internal import Foundation /// Protocol for managing authentication tokens and credentials for CloudKit Web Services. /// diff --git a/Sources/MistKit/Authentication/WebAuthTokenManager.swift b/Sources/MistKit/Authentication/WebAuthTokenManager.swift index 0976a7ec..474563e6 100644 --- a/Sources/MistKit/Authentication/WebAuthTokenManager.swift +++ b/Sources/MistKit/Authentication/WebAuthTokenManager.swift @@ -27,7 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation +internal import Foundation /// Token manager for web authentication with API token + web auth token. /// Provides user-specific access to CloudKit Web Services. diff --git a/Sources/MistKit/CloudKitService/CloudKitError.swift b/Sources/MistKit/CloudKitService/CloudKitError.swift index 3d511f6a..4b4bcf25 100644 --- a/Sources/MistKit/CloudKitService/CloudKitError.swift +++ b/Sources/MistKit/CloudKitService/CloudKitError.swift @@ -28,10 +28,10 @@ // public import Foundation -import OpenAPIRuntime +internal import OpenAPIRuntime #if canImport(FoundationNetworking) - import FoundationNetworking + internal import FoundationNetworking #endif /// Represents errors that can occur when interacting with CloudKit Web Services diff --git a/Sources/MistKit/CloudKitService/CloudKitResponseProcessor+Changes.swift b/Sources/MistKit/CloudKitService/CloudKitResponseProcessor+Changes.swift index 7d5faf8e..8a7bd2e6 100644 --- a/Sources/MistKit/CloudKitService/CloudKitResponseProcessor+Changes.swift +++ b/Sources/MistKit/CloudKitService/CloudKitResponseProcessor+Changes.swift @@ -29,7 +29,7 @@ internal import Foundation internal import MistKitOpenAPI -import OpenAPIRuntime +internal import OpenAPIRuntime extension CloudKitResponseProcessor { /// Process fetchRecordChanges response diff --git a/Sources/MistKit/CloudKitService/CloudKitResponseProcessor.swift b/Sources/MistKit/CloudKitService/CloudKitResponseProcessor.swift index dd4487b4..2e5646e2 100644 --- a/Sources/MistKit/CloudKitService/CloudKitResponseProcessor.swift +++ b/Sources/MistKit/CloudKitService/CloudKitResponseProcessor.swift @@ -30,7 +30,7 @@ internal import Foundation internal import Logging internal import MistKitOpenAPI -import OpenAPIRuntime +internal import OpenAPIRuntime /// Processes CloudKit API responses and handles errors. /// diff --git a/Sources/MistKit/CloudKitService/CloudKitService+AssetOperations.swift b/Sources/MistKit/CloudKitService/CloudKitService+AssetOperations.swift index 953a492a..99111fee 100644 --- a/Sources/MistKit/CloudKitService/CloudKitService+AssetOperations.swift +++ b/Sources/MistKit/CloudKitService/CloudKitService+AssetOperations.swift @@ -28,16 +28,16 @@ // public import Foundation -import HTTPTypes +internal import HTTPTypes internal import MistKitOpenAPI -import OpenAPIRuntime +internal import OpenAPIRuntime #if canImport(FoundationNetworking) - import FoundationNetworking + internal import FoundationNetworking #endif #if !os(WASI) - import OpenAPIURLSession + internal import OpenAPIURLSession #endif @available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *) diff --git a/Sources/MistKit/CloudKitService/CloudKitService+AssetUpload.swift b/Sources/MistKit/CloudKitService/CloudKitService+AssetUpload.swift index aba78495..6de368bf 100644 --- a/Sources/MistKit/CloudKitService/CloudKitService+AssetUpload.swift +++ b/Sources/MistKit/CloudKitService/CloudKitService+AssetUpload.swift @@ -31,11 +31,11 @@ public import Foundation internal import Logging #if canImport(FoundationNetworking) - import FoundationNetworking + internal import FoundationNetworking #endif #if !os(WASI) - import OpenAPIURLSession + internal import OpenAPIURLSession #endif @available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *) diff --git a/Sources/MistKit/CloudKitService/CloudKitService+Classification.swift b/Sources/MistKit/CloudKitService/CloudKitService+Classification.swift index f31236f5..bab81204 100644 --- a/Sources/MistKit/CloudKitService/CloudKitService+Classification.swift +++ b/Sources/MistKit/CloudKitService/CloudKitService+Classification.swift @@ -27,7 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation +internal import Foundation /// Helpers for tracking creates vs updates in `modifyRecords` responses. /// diff --git a/Sources/MistKit/CloudKitService/CloudKitService+ErrorHandling.swift b/Sources/MistKit/CloudKitService/CloudKitService+ErrorHandling.swift index 507caa63..4aa21477 100644 --- a/Sources/MistKit/CloudKitService/CloudKitService+ErrorHandling.swift +++ b/Sources/MistKit/CloudKitService/CloudKitService+ErrorHandling.swift @@ -27,12 +27,12 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation +internal import Foundation internal import Logging -import OpenAPIRuntime +internal import OpenAPIRuntime #if canImport(FoundationNetworking) - import FoundationNetworking + internal import FoundationNetworking #endif extension CloudKitService { diff --git a/Sources/MistKit/CloudKitService/CloudKitService+LookupOperations.swift b/Sources/MistKit/CloudKitService/CloudKitService+LookupOperations.swift index 4996654e..a21e5568 100644 --- a/Sources/MistKit/CloudKitService/CloudKitService+LookupOperations.swift +++ b/Sources/MistKit/CloudKitService/CloudKitService+LookupOperations.swift @@ -27,7 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation +internal import Foundation internal import MistKitOpenAPI extension CloudKitService { diff --git a/Sources/MistKit/CloudKitService/CloudKitService+ModifyZones.swift b/Sources/MistKit/CloudKitService/CloudKitService+ModifyZones.swift index bad9fd7a..73ef3a89 100644 --- a/Sources/MistKit/CloudKitService/CloudKitService+ModifyZones.swift +++ b/Sources/MistKit/CloudKitService/CloudKitService+ModifyZones.swift @@ -27,16 +27,16 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation +internal import Foundation internal import MistKitOpenAPI -import OpenAPIRuntime +internal import OpenAPIRuntime #if canImport(FoundationNetworking) - import FoundationNetworking + internal import FoundationNetworking #endif #if !os(WASI) - import OpenAPIURLSession + internal import OpenAPIURLSession #endif extension CloudKitService { diff --git a/Sources/MistKit/CloudKitService/CloudKitService+Operations.swift b/Sources/MistKit/CloudKitService/CloudKitService+Operations.swift index 2e0d6e25..0faea7e0 100644 --- a/Sources/MistKit/CloudKitService/CloudKitService+Operations.swift +++ b/Sources/MistKit/CloudKitService/CloudKitService+Operations.swift @@ -27,16 +27,16 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation +internal import Foundation internal import MistKitOpenAPI -import OpenAPIRuntime +internal import OpenAPIRuntime #if canImport(FoundationNetworking) - import FoundationNetworking + internal import FoundationNetworking #endif #if !os(WASI) - import OpenAPIURLSession + internal import OpenAPIURLSession #endif extension CloudKitService { diff --git a/Sources/MistKit/CloudKitService/CloudKitService+QueryPagination.swift b/Sources/MistKit/CloudKitService/CloudKitService+QueryPagination.swift index 3001b042..d87a7253 100644 --- a/Sources/MistKit/CloudKitService/CloudKitService+QueryPagination.swift +++ b/Sources/MistKit/CloudKitService/CloudKitService+QueryPagination.swift @@ -27,7 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation +internal import Foundation extension CloudKitService { /// Query all records, handling pagination automatically diff --git a/Sources/MistKit/CloudKitService/CloudKitService+RecordManaging.swift b/Sources/MistKit/CloudKitService/CloudKitService+RecordManaging.swift index 5c36f93f..61745820 100644 --- a/Sources/MistKit/CloudKitService/CloudKitService+RecordManaging.swift +++ b/Sources/MistKit/CloudKitService/CloudKitService+RecordManaging.swift @@ -27,7 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation +internal import Foundation /// CloudKitService conformance to RecordManaging protocol /// diff --git a/Sources/MistKit/CloudKitService/CloudKitService+SyncOperations.swift b/Sources/MistKit/CloudKitService/CloudKitService+SyncOperations.swift index 81ffa95d..ef6ecb26 100644 --- a/Sources/MistKit/CloudKitService/CloudKitService+SyncOperations.swift +++ b/Sources/MistKit/CloudKitService/CloudKitService+SyncOperations.swift @@ -27,16 +27,16 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation +internal import Foundation internal import MistKitOpenAPI -import OpenAPIRuntime +internal import OpenAPIRuntime #if canImport(FoundationNetworking) - import FoundationNetworking + internal import FoundationNetworking #endif #if !os(WASI) - import OpenAPIURLSession + internal import OpenAPIURLSession #endif extension CloudKitService { diff --git a/Sources/MistKit/CloudKitService/CloudKitService+UserOperations.swift b/Sources/MistKit/CloudKitService/CloudKitService+UserOperations.swift index ebec3824..ef57ee60 100644 --- a/Sources/MistKit/CloudKitService/CloudKitService+UserOperations.swift +++ b/Sources/MistKit/CloudKitService/CloudKitService+UserOperations.swift @@ -27,16 +27,16 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation +internal import Foundation internal import MistKitOpenAPI -import OpenAPIRuntime +internal import OpenAPIRuntime #if canImport(FoundationNetworking) - import FoundationNetworking + internal import FoundationNetworking #endif #if !os(WASI) - import OpenAPIURLSession + internal import OpenAPIURLSession #endif extension CloudKitService { diff --git a/Sources/MistKit/CloudKitService/CloudKitService+WriteOperations.swift b/Sources/MistKit/CloudKitService/CloudKitService+WriteOperations.swift index 6fd2e864..0a82e207 100644 --- a/Sources/MistKit/CloudKitService/CloudKitService+WriteOperations.swift +++ b/Sources/MistKit/CloudKitService/CloudKitService+WriteOperations.swift @@ -27,16 +27,16 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation +internal import Foundation internal import MistKitOpenAPI -import OpenAPIRuntime +internal import OpenAPIRuntime #if canImport(FoundationNetworking) - import FoundationNetworking + internal import FoundationNetworking #endif #if !os(WASI) - import OpenAPIURLSession + internal import OpenAPIURLSession #endif extension CloudKitService { diff --git a/Sources/MistKit/CloudKitService/CloudKitService+ZoneOperations.swift b/Sources/MistKit/CloudKitService/CloudKitService+ZoneOperations.swift index a180e56f..d423ffb4 100644 --- a/Sources/MistKit/CloudKitService/CloudKitService+ZoneOperations.swift +++ b/Sources/MistKit/CloudKitService/CloudKitService+ZoneOperations.swift @@ -27,16 +27,16 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation +internal import Foundation internal import MistKitOpenAPI -import OpenAPIRuntime +internal import OpenAPIRuntime #if canImport(FoundationNetworking) - import FoundationNetworking + internal import FoundationNetworking #endif #if !os(WASI) - import OpenAPIURLSession + internal import OpenAPIURLSession #endif extension CloudKitService { diff --git a/Sources/MistKit/Models/BatchSyncResult.swift b/Sources/MistKit/Models/BatchSyncResult.swift index 03fec1fc..b4d8ccdb 100644 --- a/Sources/MistKit/Models/BatchSyncResult.swift +++ b/Sources/MistKit/Models/BatchSyncResult.swift @@ -27,7 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation +internal import Foundation /// Categorized result of a tracked `modifyRecords(_:classification:atomic:)` call. /// diff --git a/Sources/MistKit/Models/OperationClassification.swift b/Sources/MistKit/Models/OperationClassification.swift index 2c4f7fd8..17154f2e 100644 --- a/Sources/MistKit/Models/OperationClassification.swift +++ b/Sources/MistKit/Models/OperationClassification.swift @@ -27,7 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation +internal import Foundation /// Classifies CloudKit record operations as creates or updates. /// diff --git a/Sources/MistKit/Models/Queries/FilterBuilder/FilterBuilder+ListMemberFilters.swift b/Sources/MistKit/Models/Queries/FilterBuilder/FilterBuilder+ListMemberFilters.swift index 2de68fbd..df0a8869 100644 --- a/Sources/MistKit/Models/Queries/FilterBuilder/FilterBuilder+ListMemberFilters.swift +++ b/Sources/MistKit/Models/Queries/FilterBuilder/FilterBuilder+ListMemberFilters.swift @@ -27,7 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation +internal import Foundation internal import MistKitOpenAPI extension FilterBuilder { diff --git a/Sources/MistKit/Models/Queries/FilterBuilder/FilterBuilder+StringFilters.swift b/Sources/MistKit/Models/Queries/FilterBuilder/FilterBuilder+StringFilters.swift index 4ba70994..689e58ee 100644 --- a/Sources/MistKit/Models/Queries/FilterBuilder/FilterBuilder+StringFilters.swift +++ b/Sources/MistKit/Models/Queries/FilterBuilder/FilterBuilder+StringFilters.swift @@ -27,7 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation +internal import Foundation internal import MistKitOpenAPI extension FilterBuilder { diff --git a/Sources/MistKit/Models/Queries/FilterBuilder/FilterBuilder.swift b/Sources/MistKit/Models/Queries/FilterBuilder/FilterBuilder.swift index 54a569ce..92795482 100644 --- a/Sources/MistKit/Models/Queries/FilterBuilder/FilterBuilder.swift +++ b/Sources/MistKit/Models/Queries/FilterBuilder/FilterBuilder.swift @@ -27,7 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation +internal import Foundation internal import MistKitOpenAPI /// A builder for constructing CloudKit query filters diff --git a/Sources/MistKit/Models/Queries/QueryFilter.swift b/Sources/MistKit/Models/Queries/QueryFilter.swift index e6669451..85774cd5 100644 --- a/Sources/MistKit/Models/Queries/QueryFilter.swift +++ b/Sources/MistKit/Models/Queries/QueryFilter.swift @@ -27,7 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation +internal import Foundation internal import MistKitOpenAPI /// Public wrapper for CloudKit query filters diff --git a/Sources/MistKit/Models/Queries/QuerySort.swift b/Sources/MistKit/Models/Queries/QuerySort.swift index cf055cff..f458843e 100644 --- a/Sources/MistKit/Models/Queries/QuerySort.swift +++ b/Sources/MistKit/Models/Queries/QuerySort.swift @@ -27,7 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation +internal import Foundation internal import MistKitOpenAPI /// Public wrapper for CloudKit query sort descriptors diff --git a/Sources/MistKit/Models/RecordOperation.swift b/Sources/MistKit/Models/RecordOperation.swift index 5424f903..a1b4b577 100644 --- a/Sources/MistKit/Models/RecordOperation.swift +++ b/Sources/MistKit/Models/RecordOperation.swift @@ -27,7 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation +internal import Foundation /// Represents a CloudKit record operation (create, update, delete, etc.) public struct RecordOperation: Sendable { diff --git a/Sources/MistKit/OpenAPI/LoggingMiddleware.swift b/Sources/MistKit/OpenAPI/LoggingMiddleware.swift index a5b2f882..ca49e034 100644 --- a/Sources/MistKit/OpenAPI/LoggingMiddleware.swift +++ b/Sources/MistKit/OpenAPI/LoggingMiddleware.swift @@ -27,10 +27,10 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import HTTPTypes +internal import Foundation +internal import HTTPTypes internal import Logging -import OpenAPIRuntime +internal import OpenAPIRuntime /// Logging middleware for HTTP request/response tracing. /// diff --git a/Sources/MistKit/RecordManagement/CloudKitRecordCollection.swift b/Sources/MistKit/RecordManagement/CloudKitRecordCollection.swift index edb2c1d2..7e09e651 100644 --- a/Sources/MistKit/RecordManagement/CloudKitRecordCollection.swift +++ b/Sources/MistKit/RecordManagement/CloudKitRecordCollection.swift @@ -27,7 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation +internal import Foundation /// Protocol for services that manage a collection of CloudKit record types using variadic generics /// diff --git a/Sources/MistKit/RecordManagement/RecordManaging+Generic.swift b/Sources/MistKit/RecordManagement/RecordManaging+Generic.swift index e03c96b2..9cf184a5 100644 --- a/Sources/MistKit/RecordManagement/RecordManaging+Generic.swift +++ b/Sources/MistKit/RecordManagement/RecordManaging+Generic.swift @@ -27,7 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation +internal import Foundation /// Generic extensions for RecordManaging protocol that work with any CloudKitRecord type /// diff --git a/Sources/MistKit/RecordManagement/RecordManaging.swift b/Sources/MistKit/RecordManagement/RecordManaging.swift index 3620a7f6..bafe1b46 100644 --- a/Sources/MistKit/RecordManagement/RecordManaging.swift +++ b/Sources/MistKit/RecordManagement/RecordManaging.swift @@ -27,7 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation +internal import Foundation /// Protocol defining core CloudKit record management operations /// diff --git a/Tests/MistKitTests/Authentication/APIToken/APITokenAuthenticatorTests.swift b/Tests/MistKitTests/Authentication/APIToken/APITokenAuthenticatorTests.swift index 7187aa71..da1f221a 100644 --- a/Tests/MistKitTests/Authentication/APIToken/APITokenAuthenticatorTests.swift +++ b/Tests/MistKitTests/Authentication/APIToken/APITokenAuthenticatorTests.swift @@ -6,10 +6,10 @@ // Copyright © 2026 BrightDigit. // -import Foundation -import HTTPTypes -import OpenAPIRuntime -import Testing +internal import Foundation +internal import HTTPTypes +internal import OpenAPIRuntime +internal import Testing @testable import MistKit diff --git a/Tests/MistKitTests/Authentication/APIToken/APITokenManager+TestHelpers.swift b/Tests/MistKitTests/Authentication/APIToken/APITokenManager+TestHelpers.swift index fb64a1f5..9152a5b6 100644 --- a/Tests/MistKitTests/Authentication/APIToken/APITokenManager+TestHelpers.swift +++ b/Tests/MistKitTests/Authentication/APIToken/APITokenManager+TestHelpers.swift @@ -1,5 +1,5 @@ -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistKit diff --git a/Tests/MistKitTests/Authentication/APIToken/APITokenManagerTests+Manager.swift b/Tests/MistKitTests/Authentication/APIToken/APITokenManagerTests+Manager.swift index 6f0fb948..01cecb34 100644 --- a/Tests/MistKitTests/Authentication/APIToken/APITokenManagerTests+Manager.swift +++ b/Tests/MistKitTests/Authentication/APIToken/APITokenManagerTests+Manager.swift @@ -1,5 +1,5 @@ -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistKit diff --git a/Tests/MistKitTests/Authentication/APIToken/APITokenManagerTests+Metadata.swift b/Tests/MistKitTests/Authentication/APIToken/APITokenManagerTests+Metadata.swift index 7de63cb7..b8f4a0f3 100644 --- a/Tests/MistKitTests/Authentication/APIToken/APITokenManagerTests+Metadata.swift +++ b/Tests/MistKitTests/Authentication/APIToken/APITokenManagerTests+Metadata.swift @@ -1,5 +1,5 @@ -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistKit diff --git a/Tests/MistKitTests/Authentication/AdaptiveTokenManager/AdaptiveTokenManager+TestHelpers.swift b/Tests/MistKitTests/Authentication/AdaptiveTokenManager/AdaptiveTokenManager+TestHelpers.swift index 8c1afd64..ae63930a 100644 --- a/Tests/MistKitTests/Authentication/AdaptiveTokenManager/AdaptiveTokenManager+TestHelpers.swift +++ b/Tests/MistKitTests/Authentication/AdaptiveTokenManager/AdaptiveTokenManager+TestHelpers.swift @@ -1,5 +1,5 @@ -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistKit diff --git a/Tests/MistKitTests/Authentication/AdaptiveTokenManager/IntegrationTests.swift b/Tests/MistKitTests/Authentication/AdaptiveTokenManager/IntegrationTests.swift index 6110528c..b8e49c70 100644 --- a/Tests/MistKitTests/Authentication/AdaptiveTokenManager/IntegrationTests.swift +++ b/Tests/MistKitTests/Authentication/AdaptiveTokenManager/IntegrationTests.swift @@ -1,5 +1,5 @@ -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistKit diff --git a/Tests/MistKitTests/Authentication/ConcurrentTokenRefresh/ConcurrentTokenRefreshTests+Basic.swift b/Tests/MistKitTests/Authentication/ConcurrentTokenRefresh/ConcurrentTokenRefreshTests+Basic.swift index 6f9f5882..f9329c2c 100644 --- a/Tests/MistKitTests/Authentication/ConcurrentTokenRefresh/ConcurrentTokenRefreshTests+Basic.swift +++ b/Tests/MistKitTests/Authentication/ConcurrentTokenRefresh/ConcurrentTokenRefreshTests+Basic.swift @@ -1,7 +1,7 @@ -import Foundation -import HTTPTypes -import OpenAPIRuntime -import Testing +internal import Foundation +internal import HTTPTypes +internal import OpenAPIRuntime +internal import Testing @testable import MistKit diff --git a/Tests/MistKitTests/Authentication/ConcurrentTokenRefresh/ConcurrentTokenRefreshTests+Error.swift b/Tests/MistKitTests/Authentication/ConcurrentTokenRefresh/ConcurrentTokenRefreshTests+Error.swift index db948395..cc89bed7 100644 --- a/Tests/MistKitTests/Authentication/ConcurrentTokenRefresh/ConcurrentTokenRefreshTests+Error.swift +++ b/Tests/MistKitTests/Authentication/ConcurrentTokenRefresh/ConcurrentTokenRefreshTests+Error.swift @@ -1,7 +1,7 @@ -import Foundation -import HTTPTypes -import OpenAPIRuntime -import Testing +internal import Foundation +internal import HTTPTypes +internal import OpenAPIRuntime +internal import Testing @testable import MistKit diff --git a/Tests/MistKitTests/Authentication/ConcurrentTokenRefresh/ConcurrentTokenRefreshTests+Helpers.swift b/Tests/MistKitTests/Authentication/ConcurrentTokenRefresh/ConcurrentTokenRefreshTests+Helpers.swift index b026177c..50b7e9fc 100644 --- a/Tests/MistKitTests/Authentication/ConcurrentTokenRefresh/ConcurrentTokenRefreshTests+Helpers.swift +++ b/Tests/MistKitTests/Authentication/ConcurrentTokenRefresh/ConcurrentTokenRefreshTests+Helpers.swift @@ -1,7 +1,7 @@ -import Foundation -import HTTPTypes -import OpenAPIRuntime -import Testing +internal import Foundation +internal import HTTPTypes +internal import OpenAPIRuntime +internal import Testing @testable import MistKit diff --git a/Tests/MistKitTests/Authentication/ConcurrentTokenRefresh/ConcurrentTokenRefreshTests+Performance.swift b/Tests/MistKitTests/Authentication/ConcurrentTokenRefresh/ConcurrentTokenRefreshTests+Performance.swift index 85644222..054b7e28 100644 --- a/Tests/MistKitTests/Authentication/ConcurrentTokenRefresh/ConcurrentTokenRefreshTests+Performance.swift +++ b/Tests/MistKitTests/Authentication/ConcurrentTokenRefresh/ConcurrentTokenRefreshTests+Performance.swift @@ -1,7 +1,7 @@ -import Foundation -import HTTPTypes -import OpenAPIRuntime -import Testing +internal import Foundation +internal import HTTPTypes +internal import OpenAPIRuntime +internal import Testing @testable import MistKit diff --git a/Tests/MistKitTests/Authentication/ConcurrentTokenRefresh/ConcurrentTokenRefreshTests.swift b/Tests/MistKitTests/Authentication/ConcurrentTokenRefresh/ConcurrentTokenRefreshTests.swift index bca44e4c..45843a49 100644 --- a/Tests/MistKitTests/Authentication/ConcurrentTokenRefresh/ConcurrentTokenRefreshTests.swift +++ b/Tests/MistKitTests/Authentication/ConcurrentTokenRefresh/ConcurrentTokenRefreshTests.swift @@ -1,4 +1,4 @@ -import Testing +internal import Testing @Suite("Concurrent Token Refresh") internal enum ConcurrentTokenRefreshTests {} diff --git a/Tests/MistKitTests/Authentication/Credentials/CredentialsTokenManagerTests+PrivateKeyLoad.swift b/Tests/MistKitTests/Authentication/Credentials/CredentialsTokenManagerTests+PrivateKeyLoad.swift index 2560f94f..524891c9 100644 --- a/Tests/MistKitTests/Authentication/Credentials/CredentialsTokenManagerTests+PrivateKeyLoad.swift +++ b/Tests/MistKitTests/Authentication/Credentials/CredentialsTokenManagerTests+PrivateKeyLoad.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistKit diff --git a/Tests/MistKitTests/Authentication/Credentials/CredentialsTokenManagerTests+PrivateShared.swift b/Tests/MistKitTests/Authentication/Credentials/CredentialsTokenManagerTests+PrivateShared.swift index 061223fc..7701140c 100644 --- a/Tests/MistKitTests/Authentication/Credentials/CredentialsTokenManagerTests+PrivateShared.swift +++ b/Tests/MistKitTests/Authentication/Credentials/CredentialsTokenManagerTests+PrivateShared.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistKit diff --git a/Tests/MistKitTests/Authentication/Credentials/CredentialsTokenManagerTests+PublicDatabase.swift b/Tests/MistKitTests/Authentication/Credentials/CredentialsTokenManagerTests+PublicDatabase.swift index 42640057..c23e4454 100644 --- a/Tests/MistKitTests/Authentication/Credentials/CredentialsTokenManagerTests+PublicDatabase.swift +++ b/Tests/MistKitTests/Authentication/Credentials/CredentialsTokenManagerTests+PublicDatabase.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistKit diff --git a/Tests/MistKitTests/Authentication/Credentials/CredentialsTokenManagerTests+UserContext.swift b/Tests/MistKitTests/Authentication/Credentials/CredentialsTokenManagerTests+UserContext.swift index 4774b0bf..5f32dace 100644 --- a/Tests/MistKitTests/Authentication/Credentials/CredentialsTokenManagerTests+UserContext.swift +++ b/Tests/MistKitTests/Authentication/Credentials/CredentialsTokenManagerTests+UserContext.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistKit diff --git a/Tests/MistKitTests/Authentication/Credentials/CredentialsTokenManagerTests.swift b/Tests/MistKitTests/Authentication/Credentials/CredentialsTokenManagerTests.swift index b13c137f..2b898795 100644 --- a/Tests/MistKitTests/Authentication/Credentials/CredentialsTokenManagerTests.swift +++ b/Tests/MistKitTests/Authentication/Credentials/CredentialsTokenManagerTests.swift @@ -27,9 +27,9 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Crypto -import Foundation -import Testing +internal import Crypto +internal import Foundation +internal import Testing @testable import MistKit diff --git a/Tests/MistKitTests/Authentication/InMemoryTokenStorage/InMemoryTokenStorage+TestHelpers.swift b/Tests/MistKitTests/Authentication/InMemoryTokenStorage/InMemoryTokenStorage+TestHelpers.swift index 3ea59378..4d955971 100644 --- a/Tests/MistKitTests/Authentication/InMemoryTokenStorage/InMemoryTokenStorage+TestHelpers.swift +++ b/Tests/MistKitTests/Authentication/InMemoryTokenStorage/InMemoryTokenStorage+TestHelpers.swift @@ -1,5 +1,5 @@ -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistKit diff --git a/Tests/MistKitTests/Authentication/InMemoryTokenStorage/InMemoryTokenStorageTests+ConcurrentRemovalTests.swift b/Tests/MistKitTests/Authentication/InMemoryTokenStorage/InMemoryTokenStorageTests+ConcurrentRemovalTests.swift index 60a43211..a5d59372 100644 --- a/Tests/MistKitTests/Authentication/InMemoryTokenStorage/InMemoryTokenStorageTests+ConcurrentRemovalTests.swift +++ b/Tests/MistKitTests/Authentication/InMemoryTokenStorage/InMemoryTokenStorageTests+ConcurrentRemovalTests.swift @@ -1,5 +1,5 @@ -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistKit diff --git a/Tests/MistKitTests/Authentication/InMemoryTokenStorage/InMemoryTokenStorageTests+ConcurrentTests.swift b/Tests/MistKitTests/Authentication/InMemoryTokenStorage/InMemoryTokenStorageTests+ConcurrentTests.swift index 9a3d5eb5..13471a93 100644 --- a/Tests/MistKitTests/Authentication/InMemoryTokenStorage/InMemoryTokenStorageTests+ConcurrentTests.swift +++ b/Tests/MistKitTests/Authentication/InMemoryTokenStorage/InMemoryTokenStorageTests+ConcurrentTests.swift @@ -1,5 +1,5 @@ -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistKit diff --git a/Tests/MistKitTests/Authentication/InMemoryTokenStorage/InMemoryTokenStorageTests+ExpirationTests.swift b/Tests/MistKitTests/Authentication/InMemoryTokenStorage/InMemoryTokenStorageTests+ExpirationTests.swift index 6b49aa98..a754788f 100644 --- a/Tests/MistKitTests/Authentication/InMemoryTokenStorage/InMemoryTokenStorageTests+ExpirationTests.swift +++ b/Tests/MistKitTests/Authentication/InMemoryTokenStorage/InMemoryTokenStorageTests+ExpirationTests.swift @@ -1,5 +1,5 @@ -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistKit diff --git a/Tests/MistKitTests/Authentication/InMemoryTokenStorage/InMemoryTokenStorageTests+InitializationTests.swift b/Tests/MistKitTests/Authentication/InMemoryTokenStorage/InMemoryTokenStorageTests+InitializationTests.swift index 56560218..43992359 100644 --- a/Tests/MistKitTests/Authentication/InMemoryTokenStorage/InMemoryTokenStorageTests+InitializationTests.swift +++ b/Tests/MistKitTests/Authentication/InMemoryTokenStorage/InMemoryTokenStorageTests+InitializationTests.swift @@ -1,6 +1,6 @@ -import Crypto -import Foundation -import Testing +internal import Crypto +internal import Foundation +internal import Testing @testable import MistKit diff --git a/Tests/MistKitTests/Authentication/InMemoryTokenStorage/InMemoryTokenStorageTests+RemovalTests.swift b/Tests/MistKitTests/Authentication/InMemoryTokenStorage/InMemoryTokenStorageTests+RemovalTests.swift index dc5d585c..faed5ac0 100644 --- a/Tests/MistKitTests/Authentication/InMemoryTokenStorage/InMemoryTokenStorageTests+RemovalTests.swift +++ b/Tests/MistKitTests/Authentication/InMemoryTokenStorage/InMemoryTokenStorageTests+RemovalTests.swift @@ -1,5 +1,5 @@ -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistKit diff --git a/Tests/MistKitTests/Authentication/InMemoryTokenStorage/InMemoryTokenStorageTests+ReplacementTests.swift b/Tests/MistKitTests/Authentication/InMemoryTokenStorage/InMemoryTokenStorageTests+ReplacementTests.swift index 5469b1f9..f40072e2 100644 --- a/Tests/MistKitTests/Authentication/InMemoryTokenStorage/InMemoryTokenStorageTests+ReplacementTests.swift +++ b/Tests/MistKitTests/Authentication/InMemoryTokenStorage/InMemoryTokenStorageTests+ReplacementTests.swift @@ -1,5 +1,5 @@ -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistKit diff --git a/Tests/MistKitTests/Authentication/InMemoryTokenStorage/InMemoryTokenStorageTests+RetrievalTests.swift b/Tests/MistKitTests/Authentication/InMemoryTokenStorage/InMemoryTokenStorageTests+RetrievalTests.swift index efa21884..63de2de5 100644 --- a/Tests/MistKitTests/Authentication/InMemoryTokenStorage/InMemoryTokenStorageTests+RetrievalTests.swift +++ b/Tests/MistKitTests/Authentication/InMemoryTokenStorage/InMemoryTokenStorageTests+RetrievalTests.swift @@ -1,5 +1,5 @@ -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistKit diff --git a/Tests/MistKitTests/Authentication/Middleware/AuthenticationMiddleware+TestHelpers.swift b/Tests/MistKitTests/Authentication/Middleware/AuthenticationMiddleware+TestHelpers.swift index abb86e79..4848c49d 100644 --- a/Tests/MistKitTests/Authentication/Middleware/AuthenticationMiddleware+TestHelpers.swift +++ b/Tests/MistKitTests/Authentication/Middleware/AuthenticationMiddleware+TestHelpers.swift @@ -1,7 +1,7 @@ -import Foundation -import HTTPTypes -import OpenAPIRuntime -import Testing +internal import Foundation +internal import HTTPTypes +internal import OpenAPIRuntime +internal import Testing @testable import MistKit diff --git a/Tests/MistKitTests/Authentication/Middleware/AuthenticationMiddlewareTests+APIToken.swift b/Tests/MistKitTests/Authentication/Middleware/AuthenticationMiddlewareTests+APIToken.swift index ecf727d9..167dd52e 100644 --- a/Tests/MistKitTests/Authentication/Middleware/AuthenticationMiddlewareTests+APIToken.swift +++ b/Tests/MistKitTests/Authentication/Middleware/AuthenticationMiddlewareTests+APIToken.swift @@ -1,8 +1,8 @@ -import Crypto -import Foundation -import HTTPTypes -import OpenAPIRuntime -import Testing +internal import Crypto +internal import Foundation +internal import HTTPTypes +internal import OpenAPIRuntime +internal import Testing @testable import MistKit diff --git a/Tests/MistKitTests/Authentication/Middleware/AuthenticationMiddlewareTests+Initialization.swift b/Tests/MistKitTests/Authentication/Middleware/AuthenticationMiddlewareTests+Initialization.swift index 5d2cb2fc..ea42b806 100644 --- a/Tests/MistKitTests/Authentication/Middleware/AuthenticationMiddlewareTests+Initialization.swift +++ b/Tests/MistKitTests/Authentication/Middleware/AuthenticationMiddlewareTests+Initialization.swift @@ -1,8 +1,8 @@ -import Crypto -import Foundation -import HTTPTypes -import OpenAPIRuntime -import Testing +internal import Crypto +internal import Foundation +internal import HTTPTypes +internal import OpenAPIRuntime +internal import Testing @testable import MistKit diff --git a/Tests/MistKitTests/Authentication/Middleware/AuthenticationMiddlewareTests+ServerToServer.swift b/Tests/MistKitTests/Authentication/Middleware/AuthenticationMiddlewareTests+ServerToServer.swift index cf85a7bc..710860b5 100644 --- a/Tests/MistKitTests/Authentication/Middleware/AuthenticationMiddlewareTests+ServerToServer.swift +++ b/Tests/MistKitTests/Authentication/Middleware/AuthenticationMiddlewareTests+ServerToServer.swift @@ -1,8 +1,8 @@ -import Crypto -import Foundation -import HTTPTypes -import OpenAPIRuntime -import Testing +internal import Crypto +internal import Foundation +internal import HTTPTypes +internal import OpenAPIRuntime +internal import Testing @testable import MistKit diff --git a/Tests/MistKitTests/Authentication/Middleware/AuthenticationMiddlewareTests+WebAuth.swift b/Tests/MistKitTests/Authentication/Middleware/AuthenticationMiddlewareTests+WebAuth.swift index 60676ca0..58146801 100644 --- a/Tests/MistKitTests/Authentication/Middleware/AuthenticationMiddlewareTests+WebAuth.swift +++ b/Tests/MistKitTests/Authentication/Middleware/AuthenticationMiddlewareTests+WebAuth.swift @@ -1,8 +1,8 @@ -import Crypto -import Foundation -import HTTPTypes -import OpenAPIRuntime -import Testing +internal import Crypto +internal import Foundation +internal import HTTPTypes +internal import OpenAPIRuntime +internal import Testing @testable import MistKit diff --git a/Tests/MistKitTests/Authentication/Middleware/AuthenticationMiddlewareTests.swift b/Tests/MistKitTests/Authentication/Middleware/AuthenticationMiddlewareTests.swift index 255b9224..3fa90199 100644 --- a/Tests/MistKitTests/Authentication/Middleware/AuthenticationMiddlewareTests.swift +++ b/Tests/MistKitTests/Authentication/Middleware/AuthenticationMiddlewareTests.swift @@ -1,4 +1,4 @@ -import Testing +internal import Testing @Suite("Authentication Middleware") internal enum AuthenticationMiddlewareTests {} diff --git a/Tests/MistKitTests/Authentication/Middleware/Error/AuthenticationMiddlewareTests+Error.swift b/Tests/MistKitTests/Authentication/Middleware/Error/AuthenticationMiddlewareTests+Error.swift index 698156f2..795a1ed5 100644 --- a/Tests/MistKitTests/Authentication/Middleware/Error/AuthenticationMiddlewareTests+Error.swift +++ b/Tests/MistKitTests/Authentication/Middleware/Error/AuthenticationMiddlewareTests+Error.swift @@ -1,8 +1,8 @@ -import Crypto -import Foundation -import HTTPTypes -import OpenAPIRuntime -import Testing +internal import Crypto +internal import Foundation +internal import HTTPTypes +internal import OpenAPIRuntime +internal import Testing @testable import MistKit diff --git a/Tests/MistKitTests/Authentication/Middleware/Error/MockTokenManagerWithNetworkError.swift b/Tests/MistKitTests/Authentication/Middleware/Error/MockTokenManagerWithNetworkError.swift index 0a1adc58..f11c1f47 100644 --- a/Tests/MistKitTests/Authentication/Middleware/Error/MockTokenManagerWithNetworkError.swift +++ b/Tests/MistKitTests/Authentication/Middleware/Error/MockTokenManagerWithNetworkError.swift @@ -5,7 +5,7 @@ // Created by Leo Dion on 9/25/25. // -import Foundation +internal import Foundation @testable import MistKit diff --git a/Tests/MistKitTests/Authentication/NetworkError/RecoveryTests.swift b/Tests/MistKitTests/Authentication/NetworkError/RecoveryTests.swift index 286a4734..8364635f 100644 --- a/Tests/MistKitTests/Authentication/NetworkError/RecoveryTests.swift +++ b/Tests/MistKitTests/Authentication/NetworkError/RecoveryTests.swift @@ -1,8 +1,8 @@ -import Crypto -import Foundation -import HTTPTypes -import OpenAPIRuntime -import Testing +internal import Crypto +internal import Foundation +internal import HTTPTypes +internal import OpenAPIRuntime +internal import Testing @testable import MistKit diff --git a/Tests/MistKitTests/Authentication/NetworkError/SimulationTests.swift b/Tests/MistKitTests/Authentication/NetworkError/SimulationTests.swift index 74711c29..45a040aa 100644 --- a/Tests/MistKitTests/Authentication/NetworkError/SimulationTests.swift +++ b/Tests/MistKitTests/Authentication/NetworkError/SimulationTests.swift @@ -1,8 +1,8 @@ -import Crypto -import Foundation -import HTTPTypes -import OpenAPIRuntime -import Testing +internal import Crypto +internal import Foundation +internal import HTTPTypes +internal import OpenAPIRuntime +internal import Testing @testable import MistKit diff --git a/Tests/MistKitTests/Authentication/NetworkError/StorageTests.swift b/Tests/MistKitTests/Authentication/NetworkError/StorageTests.swift index 629315e5..2cf06a62 100644 --- a/Tests/MistKitTests/Authentication/NetworkError/StorageTests.swift +++ b/Tests/MistKitTests/Authentication/NetworkError/StorageTests.swift @@ -1,8 +1,8 @@ -import Crypto -import Foundation -import HTTPTypes -import OpenAPIRuntime -import Testing +internal import Crypto +internal import Foundation +internal import HTTPTypes +internal import OpenAPIRuntime +internal import Testing @testable import MistKit diff --git a/Tests/MistKitTests/Authentication/ServerToServer/ServerToServerAuthManager+TestHelpers.swift b/Tests/MistKitTests/Authentication/ServerToServer/ServerToServerAuthManager+TestHelpers.swift index dec97e9d..117e48a2 100644 --- a/Tests/MistKitTests/Authentication/ServerToServer/ServerToServerAuthManager+TestHelpers.swift +++ b/Tests/MistKitTests/Authentication/ServerToServer/ServerToServerAuthManager+TestHelpers.swift @@ -1,5 +1,5 @@ -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistKit diff --git a/Tests/MistKitTests/Authentication/ServerToServer/ServerToServerAuthManagerTests+ErrorTests.swift b/Tests/MistKitTests/Authentication/ServerToServer/ServerToServerAuthManagerTests+ErrorTests.swift index 2b178d23..15e6a22d 100644 --- a/Tests/MistKitTests/Authentication/ServerToServer/ServerToServerAuthManagerTests+ErrorTests.swift +++ b/Tests/MistKitTests/Authentication/ServerToServer/ServerToServerAuthManagerTests+ErrorTests.swift @@ -1,6 +1,6 @@ -import Crypto -import Foundation -import Testing +internal import Crypto +internal import Foundation +internal import Testing @testable import MistKit diff --git a/Tests/MistKitTests/Authentication/ServerToServer/ServerToServerAuthManagerTests+InitializationTests.swift b/Tests/MistKitTests/Authentication/ServerToServer/ServerToServerAuthManagerTests+InitializationTests.swift index ed74025a..e78c2d2c 100644 --- a/Tests/MistKitTests/Authentication/ServerToServer/ServerToServerAuthManagerTests+InitializationTests.swift +++ b/Tests/MistKitTests/Authentication/ServerToServer/ServerToServerAuthManagerTests+InitializationTests.swift @@ -1,6 +1,6 @@ -import Crypto -import Foundation -import Testing +internal import Crypto +internal import Foundation +internal import Testing @testable import MistKit diff --git a/Tests/MistKitTests/Authentication/ServerToServer/ServerToServerAuthManagerTests+PrivateKeyTests.swift b/Tests/MistKitTests/Authentication/ServerToServer/ServerToServerAuthManagerTests+PrivateKeyTests.swift index 391b3905..44c612e8 100644 --- a/Tests/MistKitTests/Authentication/ServerToServer/ServerToServerAuthManagerTests+PrivateKeyTests.swift +++ b/Tests/MistKitTests/Authentication/ServerToServer/ServerToServerAuthManagerTests+PrivateKeyTests.swift @@ -1,6 +1,6 @@ -import Crypto -import Foundation -import Testing +internal import Crypto +internal import Foundation +internal import Testing @testable import MistKit diff --git a/Tests/MistKitTests/Authentication/ServerToServer/ServerToServerAuthManagerTests+ValidationTests.swift b/Tests/MistKitTests/Authentication/ServerToServer/ServerToServerAuthManagerTests+ValidationTests.swift index 888c80da..500d9564 100644 --- a/Tests/MistKitTests/Authentication/ServerToServer/ServerToServerAuthManagerTests+ValidationTests.swift +++ b/Tests/MistKitTests/Authentication/ServerToServer/ServerToServerAuthManagerTests+ValidationTests.swift @@ -1,6 +1,6 @@ -import Crypto -import Foundation -import Testing +internal import Crypto +internal import Foundation +internal import Testing @testable import MistKit diff --git a/Tests/MistKitTests/Authentication/ServerToServer/ServerToServerAuthenticatorTests.swift b/Tests/MistKitTests/Authentication/ServerToServer/ServerToServerAuthenticatorTests.swift index 88bb5351..b909f6ab 100644 --- a/Tests/MistKitTests/Authentication/ServerToServer/ServerToServerAuthenticatorTests.swift +++ b/Tests/MistKitTests/Authentication/ServerToServer/ServerToServerAuthenticatorTests.swift @@ -6,11 +6,11 @@ // Copyright © 2026 BrightDigit. // -import Crypto -import Foundation -import HTTPTypes -import OpenAPIRuntime -import Testing +internal import Crypto +internal import Foundation +internal import HTTPTypes +internal import OpenAPIRuntime +internal import Testing @testable import MistKit diff --git a/Tests/MistKitTests/Authentication/TokenManager/TokenManagerError+TestHelpers.swift b/Tests/MistKitTests/Authentication/TokenManager/TokenManagerError+TestHelpers.swift index e3aab9d7..043d645c 100644 --- a/Tests/MistKitTests/Authentication/TokenManager/TokenManagerError+TestHelpers.swift +++ b/Tests/MistKitTests/Authentication/TokenManager/TokenManagerError+TestHelpers.swift @@ -1,5 +1,5 @@ -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistKit diff --git a/Tests/MistKitTests/Authentication/TokenManager/TokenManagerErrorTests.swift b/Tests/MistKitTests/Authentication/TokenManager/TokenManagerErrorTests.swift index de862839..f33369df 100644 --- a/Tests/MistKitTests/Authentication/TokenManager/TokenManagerErrorTests.swift +++ b/Tests/MistKitTests/Authentication/TokenManager/TokenManagerErrorTests.swift @@ -1,5 +1,5 @@ -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistKit diff --git a/Tests/MistKitTests/Authentication/TokenManager/TokenManagerProtocolTests.swift b/Tests/MistKitTests/Authentication/TokenManager/TokenManagerProtocolTests.swift index 4562a54a..69e9a7da 100644 --- a/Tests/MistKitTests/Authentication/TokenManager/TokenManagerProtocolTests.swift +++ b/Tests/MistKitTests/Authentication/TokenManager/TokenManagerProtocolTests.swift @@ -1,5 +1,5 @@ -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistKit diff --git a/Tests/MistKitTests/Authentication/TokenManager/TokenManagerTests.swift b/Tests/MistKitTests/Authentication/TokenManager/TokenManagerTests.swift index f5df6154..185474ba 100644 --- a/Tests/MistKitTests/Authentication/TokenManager/TokenManagerTests.swift +++ b/Tests/MistKitTests/Authentication/TokenManager/TokenManagerTests.swift @@ -1,5 +1,5 @@ -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistKit diff --git a/Tests/MistKitTests/Authentication/WebAuth/WebAuthTokenAuthenticatorTests.swift b/Tests/MistKitTests/Authentication/WebAuth/WebAuthTokenAuthenticatorTests.swift index abad9e5b..28fd731a 100644 --- a/Tests/MistKitTests/Authentication/WebAuth/WebAuthTokenAuthenticatorTests.swift +++ b/Tests/MistKitTests/Authentication/WebAuth/WebAuthTokenAuthenticatorTests.swift @@ -6,10 +6,10 @@ // Copyright © 2026 BrightDigit. // -import Foundation -import HTTPTypes -import OpenAPIRuntime -import Testing +internal import Foundation +internal import HTTPTypes +internal import OpenAPIRuntime +internal import Testing @testable import MistKit diff --git a/Tests/MistKitTests/Authentication/WebAuth/WebAuthTokenManager+TestHelpers.swift b/Tests/MistKitTests/Authentication/WebAuth/WebAuthTokenManager+TestHelpers.swift index 03f3bfab..82300ea3 100644 --- a/Tests/MistKitTests/Authentication/WebAuth/WebAuthTokenManager+TestHelpers.swift +++ b/Tests/MistKitTests/Authentication/WebAuth/WebAuthTokenManager+TestHelpers.swift @@ -1,5 +1,5 @@ -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistKit diff --git a/Tests/MistKitTests/Authentication/WebAuth/WebAuthTokenManagerTests+Basic.swift b/Tests/MistKitTests/Authentication/WebAuth/WebAuthTokenManagerTests+Basic.swift index 520ef412..50958e94 100644 --- a/Tests/MistKitTests/Authentication/WebAuth/WebAuthTokenManagerTests+Basic.swift +++ b/Tests/MistKitTests/Authentication/WebAuth/WebAuthTokenManagerTests+Basic.swift @@ -1,5 +1,5 @@ -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistKit diff --git a/Tests/MistKitTests/Authentication/WebAuth/WebAuthTokenManagerTests+EdgeCases.swift b/Tests/MistKitTests/Authentication/WebAuth/WebAuthTokenManagerTests+EdgeCases.swift index 3082dbeb..774a5c74 100644 --- a/Tests/MistKitTests/Authentication/WebAuth/WebAuthTokenManagerTests+EdgeCases.swift +++ b/Tests/MistKitTests/Authentication/WebAuth/WebAuthTokenManagerTests+EdgeCases.swift @@ -1,5 +1,5 @@ -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistKit diff --git a/Tests/MistKitTests/Authentication/WebAuth/WebAuthTokenManagerTests+Performance.swift b/Tests/MistKitTests/Authentication/WebAuth/WebAuthTokenManagerTests+Performance.swift index e8a3d6f0..c433f3ae 100644 --- a/Tests/MistKitTests/Authentication/WebAuth/WebAuthTokenManagerTests+Performance.swift +++ b/Tests/MistKitTests/Authentication/WebAuth/WebAuthTokenManagerTests+Performance.swift @@ -1,5 +1,5 @@ -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistKit diff --git a/Tests/MistKitTests/Authentication/WebAuth/WebAuthTokenManagerTests+ValidationCredentialTests.swift b/Tests/MistKitTests/Authentication/WebAuth/WebAuthTokenManagerTests+ValidationCredentialTests.swift index 1196ac6b..724c1112 100644 --- a/Tests/MistKitTests/Authentication/WebAuth/WebAuthTokenManagerTests+ValidationCredentialTests.swift +++ b/Tests/MistKitTests/Authentication/WebAuth/WebAuthTokenManagerTests+ValidationCredentialTests.swift @@ -1,5 +1,5 @@ -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistKit diff --git a/Tests/MistKitTests/Authentication/WebAuth/WebAuthTokenManagerTests+ValidationFormat.swift b/Tests/MistKitTests/Authentication/WebAuth/WebAuthTokenManagerTests+ValidationFormat.swift index 5e112d41..a1c8b0fc 100644 --- a/Tests/MistKitTests/Authentication/WebAuth/WebAuthTokenManagerTests+ValidationFormat.swift +++ b/Tests/MistKitTests/Authentication/WebAuth/WebAuthTokenManagerTests+ValidationFormat.swift @@ -1,5 +1,5 @@ -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistKit diff --git a/Tests/MistKitTests/Authentication/WebAuth/WebAuthTokenManagerTests+ValidationWorkflow.swift b/Tests/MistKitTests/Authentication/WebAuth/WebAuthTokenManagerTests+ValidationWorkflow.swift index a3244886..be3dbcf2 100644 --- a/Tests/MistKitTests/Authentication/WebAuth/WebAuthTokenManagerTests+ValidationWorkflow.swift +++ b/Tests/MistKitTests/Authentication/WebAuth/WebAuthTokenManagerTests+ValidationWorkflow.swift @@ -1,5 +1,5 @@ -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistKit diff --git a/Tests/MistKitTests/Authentication/WebAuth/WebAuthTokenManagerTests+WebAuthEdgeCases.swift b/Tests/MistKitTests/Authentication/WebAuth/WebAuthTokenManagerTests+WebAuthEdgeCases.swift index 9e0a92fd..0bab8616 100644 --- a/Tests/MistKitTests/Authentication/WebAuth/WebAuthTokenManagerTests+WebAuthEdgeCases.swift +++ b/Tests/MistKitTests/Authentication/WebAuth/WebAuthTokenManagerTests+WebAuthEdgeCases.swift @@ -1,5 +1,5 @@ -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistKit diff --git a/Tests/MistKitTests/CloudKitService/CloudKitErrorTests.swift b/Tests/MistKitTests/CloudKitService/CloudKitErrorTests.swift index b3ebc9c6..cc68b1a6 100644 --- a/Tests/MistKitTests/CloudKitService/CloudKitErrorTests.swift +++ b/Tests/MistKitTests/CloudKitService/CloudKitErrorTests.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistKit diff --git a/Tests/MistKitTests/CloudKitService/CloudKitServiceTests+Helpers.swift b/Tests/MistKitTests/CloudKitService/CloudKitServiceTests+Helpers.swift index 867fc0f9..5df7d889 100644 --- a/Tests/MistKitTests/CloudKitService/CloudKitServiceTests+Helpers.swift +++ b/Tests/MistKitTests/CloudKitService/CloudKitServiceTests+Helpers.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistKit diff --git a/Tests/MistKitTests/CloudKitService/DiscoverUserIdentities/CloudKitServiceTests.DiscoverUserIdentities+Helpers.swift b/Tests/MistKitTests/CloudKitService/DiscoverUserIdentities/CloudKitServiceTests.DiscoverUserIdentities+Helpers.swift index eaff3191..c81d49b2 100644 --- a/Tests/MistKitTests/CloudKitService/DiscoverUserIdentities/CloudKitServiceTests.DiscoverUserIdentities+Helpers.swift +++ b/Tests/MistKitTests/CloudKitService/DiscoverUserIdentities/CloudKitServiceTests.DiscoverUserIdentities+Helpers.swift @@ -27,9 +27,9 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import HTTPTypes -import Testing +internal import Foundation +internal import HTTPTypes +internal import Testing @testable import MistKit diff --git a/Tests/MistKitTests/CloudKitService/DiscoverUserIdentities/CloudKitServiceTests.DiscoverUserIdentities+InvalidEmail.swift b/Tests/MistKitTests/CloudKitService/DiscoverUserIdentities/CloudKitServiceTests.DiscoverUserIdentities+InvalidEmail.swift index 340751fb..6b2efe6d 100644 --- a/Tests/MistKitTests/CloudKitService/DiscoverUserIdentities/CloudKitServiceTests.DiscoverUserIdentities+InvalidEmail.swift +++ b/Tests/MistKitTests/CloudKitService/DiscoverUserIdentities/CloudKitServiceTests.DiscoverUserIdentities+InvalidEmail.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistKit diff --git a/Tests/MistKitTests/CloudKitService/DiscoverUserIdentities/CloudKitServiceTests.DiscoverUserIdentities+SuccessCases.swift b/Tests/MistKitTests/CloudKitService/DiscoverUserIdentities/CloudKitServiceTests.DiscoverUserIdentities+SuccessCases.swift index 2b6bb39f..6aa3dc1f 100644 --- a/Tests/MistKitTests/CloudKitService/DiscoverUserIdentities/CloudKitServiceTests.DiscoverUserIdentities+SuccessCases.swift +++ b/Tests/MistKitTests/CloudKitService/DiscoverUserIdentities/CloudKitServiceTests.DiscoverUserIdentities+SuccessCases.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistKit diff --git a/Tests/MistKitTests/CloudKitService/DiscoverUserIdentities/CloudKitServiceTests.DiscoverUserIdentities+Validation.swift b/Tests/MistKitTests/CloudKitService/DiscoverUserIdentities/CloudKitServiceTests.DiscoverUserIdentities+Validation.swift index 3938c2b1..9167f938 100644 --- a/Tests/MistKitTests/CloudKitService/DiscoverUserIdentities/CloudKitServiceTests.DiscoverUserIdentities+Validation.swift +++ b/Tests/MistKitTests/CloudKitService/DiscoverUserIdentities/CloudKitServiceTests.DiscoverUserIdentities+Validation.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistKit diff --git a/Tests/MistKitTests/CloudKitService/DiscoverUserIdentities/CloudKitServiceTests.DiscoverUserIdentities.swift b/Tests/MistKitTests/CloudKitService/DiscoverUserIdentities/CloudKitServiceTests.DiscoverUserIdentities.swift index b5d49995..73415ff1 100644 --- a/Tests/MistKitTests/CloudKitService/DiscoverUserIdentities/CloudKitServiceTests.DiscoverUserIdentities.swift +++ b/Tests/MistKitTests/CloudKitService/DiscoverUserIdentities/CloudKitServiceTests.DiscoverUserIdentities.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistKit diff --git a/Tests/MistKitTests/CloudKitService/FetchCaller/CloudKitServiceTests.FetchCaller+Helpers.swift b/Tests/MistKitTests/CloudKitService/FetchCaller/CloudKitServiceTests.FetchCaller+Helpers.swift index a8621dcf..95c31810 100644 --- a/Tests/MistKitTests/CloudKitService/FetchCaller/CloudKitServiceTests.FetchCaller+Helpers.swift +++ b/Tests/MistKitTests/CloudKitService/FetchCaller/CloudKitServiceTests.FetchCaller+Helpers.swift @@ -27,9 +27,9 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import HTTPTypes -import Testing +internal import Foundation +internal import HTTPTypes +internal import Testing @testable import MistKit diff --git a/Tests/MistKitTests/CloudKitService/FetchCaller/CloudKitServiceTests.FetchCaller+SuccessCases.swift b/Tests/MistKitTests/CloudKitService/FetchCaller/CloudKitServiceTests.FetchCaller+SuccessCases.swift index b36b2afc..2389cbaa 100644 --- a/Tests/MistKitTests/CloudKitService/FetchCaller/CloudKitServiceTests.FetchCaller+SuccessCases.swift +++ b/Tests/MistKitTests/CloudKitService/FetchCaller/CloudKitServiceTests.FetchCaller+SuccessCases.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistKit diff --git a/Tests/MistKitTests/CloudKitService/FetchCaller/CloudKitServiceTests.FetchCaller+Validation.swift b/Tests/MistKitTests/CloudKitService/FetchCaller/CloudKitServiceTests.FetchCaller+Validation.swift index 6016f062..67a73b77 100644 --- a/Tests/MistKitTests/CloudKitService/FetchCaller/CloudKitServiceTests.FetchCaller+Validation.swift +++ b/Tests/MistKitTests/CloudKitService/FetchCaller/CloudKitServiceTests.FetchCaller+Validation.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistKit diff --git a/Tests/MistKitTests/CloudKitService/FetchCaller/CloudKitServiceTests.FetchCaller.swift b/Tests/MistKitTests/CloudKitService/FetchCaller/CloudKitServiceTests.FetchCaller.swift index 0a6c9566..0bfae334 100644 --- a/Tests/MistKitTests/CloudKitService/FetchCaller/CloudKitServiceTests.FetchCaller.swift +++ b/Tests/MistKitTests/CloudKitService/FetchCaller/CloudKitServiceTests.FetchCaller.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistKit diff --git a/Tests/MistKitTests/CloudKitService/FetchChanges/CloudKitServiceTests.FetchChanges+Concurrent.swift b/Tests/MistKitTests/CloudKitService/FetchChanges/CloudKitServiceTests.FetchChanges+Concurrent.swift index 82cb0ba9..83cc385c 100644 --- a/Tests/MistKitTests/CloudKitService/FetchChanges/CloudKitServiceTests.FetchChanges+Concurrent.swift +++ b/Tests/MistKitTests/CloudKitService/FetchChanges/CloudKitServiceTests.FetchChanges+Concurrent.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistKit diff --git a/Tests/MistKitTests/CloudKitService/FetchChanges/CloudKitServiceTests.FetchChanges+ErrorHandling.swift b/Tests/MistKitTests/CloudKitService/FetchChanges/CloudKitServiceTests.FetchChanges+ErrorHandling.swift index d89a45e2..cfc1a127 100644 --- a/Tests/MistKitTests/CloudKitService/FetchChanges/CloudKitServiceTests.FetchChanges+ErrorHandling.swift +++ b/Tests/MistKitTests/CloudKitService/FetchChanges/CloudKitServiceTests.FetchChanges+ErrorHandling.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistKit diff --git a/Tests/MistKitTests/CloudKitService/FetchChanges/CloudKitServiceTests.FetchChanges+Helpers.swift b/Tests/MistKitTests/CloudKitService/FetchChanges/CloudKitServiceTests.FetchChanges+Helpers.swift index e8f64c76..6dd00917 100644 --- a/Tests/MistKitTests/CloudKitService/FetchChanges/CloudKitServiceTests.FetchChanges+Helpers.swift +++ b/Tests/MistKitTests/CloudKitService/FetchChanges/CloudKitServiceTests.FetchChanges+Helpers.swift @@ -27,9 +27,9 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import HTTPTypes -import Testing +internal import Foundation +internal import HTTPTypes +internal import Testing @testable import MistKit diff --git a/Tests/MistKitTests/CloudKitService/FetchChanges/CloudKitServiceTests.FetchChanges+SuccessCases.swift b/Tests/MistKitTests/CloudKitService/FetchChanges/CloudKitServiceTests.FetchChanges+SuccessCases.swift index c6b58da2..d23939cc 100644 --- a/Tests/MistKitTests/CloudKitService/FetchChanges/CloudKitServiceTests.FetchChanges+SuccessCases.swift +++ b/Tests/MistKitTests/CloudKitService/FetchChanges/CloudKitServiceTests.FetchChanges+SuccessCases.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistKit diff --git a/Tests/MistKitTests/CloudKitService/FetchChanges/CloudKitServiceTests.FetchChanges+Validation.swift b/Tests/MistKitTests/CloudKitService/FetchChanges/CloudKitServiceTests.FetchChanges+Validation.swift index 20644f91..0c6d8f39 100644 --- a/Tests/MistKitTests/CloudKitService/FetchChanges/CloudKitServiceTests.FetchChanges+Validation.swift +++ b/Tests/MistKitTests/CloudKitService/FetchChanges/CloudKitServiceTests.FetchChanges+Validation.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistKit diff --git a/Tests/MistKitTests/CloudKitService/FetchChanges/CloudKitServiceTests.FetchChanges.SuccessCases+Pagination.swift b/Tests/MistKitTests/CloudKitService/FetchChanges/CloudKitServiceTests.FetchChanges.SuccessCases+Pagination.swift index 3cee686e..7701cb18 100644 --- a/Tests/MistKitTests/CloudKitService/FetchChanges/CloudKitServiceTests.FetchChanges.SuccessCases+Pagination.swift +++ b/Tests/MistKitTests/CloudKitService/FetchChanges/CloudKitServiceTests.FetchChanges.SuccessCases+Pagination.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistKit diff --git a/Tests/MistKitTests/CloudKitService/FetchChanges/CloudKitServiceTests.FetchChanges.swift b/Tests/MistKitTests/CloudKitService/FetchChanges/CloudKitServiceTests.FetchChanges.swift index 201a06db..edcb97a9 100644 --- a/Tests/MistKitTests/CloudKitService/FetchChanges/CloudKitServiceTests.FetchChanges.swift +++ b/Tests/MistKitTests/CloudKitService/FetchChanges/CloudKitServiceTests.FetchChanges.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistKit diff --git a/Tests/MistKitTests/CloudKitService/FetchZoneChanges/CloudKitServiceTests.FetchZoneChanges+ErrorHandling.swift b/Tests/MistKitTests/CloudKitService/FetchZoneChanges/CloudKitServiceTests.FetchZoneChanges+ErrorHandling.swift index d3c5369b..611f3b95 100644 --- a/Tests/MistKitTests/CloudKitService/FetchZoneChanges/CloudKitServiceTests.FetchZoneChanges+ErrorHandling.swift +++ b/Tests/MistKitTests/CloudKitService/FetchZoneChanges/CloudKitServiceTests.FetchZoneChanges+ErrorHandling.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistKit diff --git a/Tests/MistKitTests/CloudKitService/FetchZoneChanges/CloudKitServiceTests.FetchZoneChanges+Helpers.swift b/Tests/MistKitTests/CloudKitService/FetchZoneChanges/CloudKitServiceTests.FetchZoneChanges+Helpers.swift index 38da0d21..01c2a419 100644 --- a/Tests/MistKitTests/CloudKitService/FetchZoneChanges/CloudKitServiceTests.FetchZoneChanges+Helpers.swift +++ b/Tests/MistKitTests/CloudKitService/FetchZoneChanges/CloudKitServiceTests.FetchZoneChanges+Helpers.swift @@ -27,9 +27,9 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import HTTPTypes -import Testing +internal import Foundation +internal import HTTPTypes +internal import Testing @testable import MistKit diff --git a/Tests/MistKitTests/CloudKitService/FetchZoneChanges/CloudKitServiceTests.FetchZoneChanges+SuccessCases.swift b/Tests/MistKitTests/CloudKitService/FetchZoneChanges/CloudKitServiceTests.FetchZoneChanges+SuccessCases.swift index 07fb20e6..ce40da10 100644 --- a/Tests/MistKitTests/CloudKitService/FetchZoneChanges/CloudKitServiceTests.FetchZoneChanges+SuccessCases.swift +++ b/Tests/MistKitTests/CloudKitService/FetchZoneChanges/CloudKitServiceTests.FetchZoneChanges+SuccessCases.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistKit diff --git a/Tests/MistKitTests/CloudKitService/FetchZoneChanges/CloudKitServiceTests.FetchZoneChanges+Validation.swift b/Tests/MistKitTests/CloudKitService/FetchZoneChanges/CloudKitServiceTests.FetchZoneChanges+Validation.swift index a7163027..32ac39e9 100644 --- a/Tests/MistKitTests/CloudKitService/FetchZoneChanges/CloudKitServiceTests.FetchZoneChanges+Validation.swift +++ b/Tests/MistKitTests/CloudKitService/FetchZoneChanges/CloudKitServiceTests.FetchZoneChanges+Validation.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistKit diff --git a/Tests/MistKitTests/CloudKitService/FetchZoneChanges/CloudKitServiceTests.FetchZoneChanges.swift b/Tests/MistKitTests/CloudKitService/FetchZoneChanges/CloudKitServiceTests.FetchZoneChanges.swift index c952dfba..a4ac0c58 100644 --- a/Tests/MistKitTests/CloudKitService/FetchZoneChanges/CloudKitServiceTests.FetchZoneChanges.swift +++ b/Tests/MistKitTests/CloudKitService/FetchZoneChanges/CloudKitServiceTests.FetchZoneChanges.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistKit diff --git a/Tests/MistKitTests/CloudKitService/LookupUsersByEmail/CloudKitServiceTests.LookupUsersByEmail+Helpers.swift b/Tests/MistKitTests/CloudKitService/LookupUsersByEmail/CloudKitServiceTests.LookupUsersByEmail+Helpers.swift index 1bdc56d2..b925f1fb 100644 --- a/Tests/MistKitTests/CloudKitService/LookupUsersByEmail/CloudKitServiceTests.LookupUsersByEmail+Helpers.swift +++ b/Tests/MistKitTests/CloudKitService/LookupUsersByEmail/CloudKitServiceTests.LookupUsersByEmail+Helpers.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistKit diff --git a/Tests/MistKitTests/CloudKitService/LookupUsersByEmail/CloudKitServiceTests.LookupUsersByEmail+SuccessCases.swift b/Tests/MistKitTests/CloudKitService/LookupUsersByEmail/CloudKitServiceTests.LookupUsersByEmail+SuccessCases.swift index 4d03e826..3796a072 100644 --- a/Tests/MistKitTests/CloudKitService/LookupUsersByEmail/CloudKitServiceTests.LookupUsersByEmail+SuccessCases.swift +++ b/Tests/MistKitTests/CloudKitService/LookupUsersByEmail/CloudKitServiceTests.LookupUsersByEmail+SuccessCases.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistKit diff --git a/Tests/MistKitTests/CloudKitService/LookupUsersByEmail/CloudKitServiceTests.LookupUsersByEmail+Validation.swift b/Tests/MistKitTests/CloudKitService/LookupUsersByEmail/CloudKitServiceTests.LookupUsersByEmail+Validation.swift index aa24cac1..c2bd71de 100644 --- a/Tests/MistKitTests/CloudKitService/LookupUsersByEmail/CloudKitServiceTests.LookupUsersByEmail+Validation.swift +++ b/Tests/MistKitTests/CloudKitService/LookupUsersByEmail/CloudKitServiceTests.LookupUsersByEmail+Validation.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistKit diff --git a/Tests/MistKitTests/CloudKitService/LookupUsersByEmail/CloudKitServiceTests.LookupUsersByEmail.swift b/Tests/MistKitTests/CloudKitService/LookupUsersByEmail/CloudKitServiceTests.LookupUsersByEmail.swift index 6fa2fcf6..5dd43af5 100644 --- a/Tests/MistKitTests/CloudKitService/LookupUsersByEmail/CloudKitServiceTests.LookupUsersByEmail.swift +++ b/Tests/MistKitTests/CloudKitService/LookupUsersByEmail/CloudKitServiceTests.LookupUsersByEmail.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistKit diff --git a/Tests/MistKitTests/CloudKitService/LookupUsersByRecordName/CloudKitServiceTests.LookupUsersByRecordName+Helpers.swift b/Tests/MistKitTests/CloudKitService/LookupUsersByRecordName/CloudKitServiceTests.LookupUsersByRecordName+Helpers.swift index a15589d0..e7c816f0 100644 --- a/Tests/MistKitTests/CloudKitService/LookupUsersByRecordName/CloudKitServiceTests.LookupUsersByRecordName+Helpers.swift +++ b/Tests/MistKitTests/CloudKitService/LookupUsersByRecordName/CloudKitServiceTests.LookupUsersByRecordName+Helpers.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistKit diff --git a/Tests/MistKitTests/CloudKitService/LookupUsersByRecordName/CloudKitServiceTests.LookupUsersByRecordName+SuccessCases.swift b/Tests/MistKitTests/CloudKitService/LookupUsersByRecordName/CloudKitServiceTests.LookupUsersByRecordName+SuccessCases.swift index dd620b66..063dbe0b 100644 --- a/Tests/MistKitTests/CloudKitService/LookupUsersByRecordName/CloudKitServiceTests.LookupUsersByRecordName+SuccessCases.swift +++ b/Tests/MistKitTests/CloudKitService/LookupUsersByRecordName/CloudKitServiceTests.LookupUsersByRecordName+SuccessCases.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistKit diff --git a/Tests/MistKitTests/CloudKitService/LookupUsersByRecordName/CloudKitServiceTests.LookupUsersByRecordName+Validation.swift b/Tests/MistKitTests/CloudKitService/LookupUsersByRecordName/CloudKitServiceTests.LookupUsersByRecordName+Validation.swift index cfccf5c6..cd58cd3c 100644 --- a/Tests/MistKitTests/CloudKitService/LookupUsersByRecordName/CloudKitServiceTests.LookupUsersByRecordName+Validation.swift +++ b/Tests/MistKitTests/CloudKitService/LookupUsersByRecordName/CloudKitServiceTests.LookupUsersByRecordName+Validation.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistKit diff --git a/Tests/MistKitTests/CloudKitService/LookupUsersByRecordName/CloudKitServiceTests.LookupUsersByRecordName.swift b/Tests/MistKitTests/CloudKitService/LookupUsersByRecordName/CloudKitServiceTests.LookupUsersByRecordName.swift index 8bd53028..db93f50d 100644 --- a/Tests/MistKitTests/CloudKitService/LookupUsersByRecordName/CloudKitServiceTests.LookupUsersByRecordName.swift +++ b/Tests/MistKitTests/CloudKitService/LookupUsersByRecordName/CloudKitServiceTests.LookupUsersByRecordName.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistKit diff --git a/Tests/MistKitTests/CloudKitService/LookupZones/CloudKitServiceTests.LookupZones+ErrorHandling.swift b/Tests/MistKitTests/CloudKitService/LookupZones/CloudKitServiceTests.LookupZones+ErrorHandling.swift index 970d3bb4..96fe18a5 100644 --- a/Tests/MistKitTests/CloudKitService/LookupZones/CloudKitServiceTests.LookupZones+ErrorHandling.swift +++ b/Tests/MistKitTests/CloudKitService/LookupZones/CloudKitServiceTests.LookupZones+ErrorHandling.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistKit diff --git a/Tests/MistKitTests/CloudKitService/LookupZones/CloudKitServiceTests.LookupZones+Helpers.swift b/Tests/MistKitTests/CloudKitService/LookupZones/CloudKitServiceTests.LookupZones+Helpers.swift index bbccb4eb..53e622fe 100644 --- a/Tests/MistKitTests/CloudKitService/LookupZones/CloudKitServiceTests.LookupZones+Helpers.swift +++ b/Tests/MistKitTests/CloudKitService/LookupZones/CloudKitServiceTests.LookupZones+Helpers.swift @@ -27,9 +27,9 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import HTTPTypes -import Testing +internal import Foundation +internal import HTTPTypes +internal import Testing @testable import MistKit diff --git a/Tests/MistKitTests/CloudKitService/LookupZones/CloudKitServiceTests.LookupZones+SuccessCases.swift b/Tests/MistKitTests/CloudKitService/LookupZones/CloudKitServiceTests.LookupZones+SuccessCases.swift index 45ac6149..8ec552e5 100644 --- a/Tests/MistKitTests/CloudKitService/LookupZones/CloudKitServiceTests.LookupZones+SuccessCases.swift +++ b/Tests/MistKitTests/CloudKitService/LookupZones/CloudKitServiceTests.LookupZones+SuccessCases.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistKit diff --git a/Tests/MistKitTests/CloudKitService/LookupZones/CloudKitServiceTests.LookupZones.swift b/Tests/MistKitTests/CloudKitService/LookupZones/CloudKitServiceTests.LookupZones.swift index 14505d9d..1f24a7ff 100644 --- a/Tests/MistKitTests/CloudKitService/LookupZones/CloudKitServiceTests.LookupZones.swift +++ b/Tests/MistKitTests/CloudKitService/LookupZones/CloudKitServiceTests.LookupZones.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistKit diff --git a/Tests/MistKitTests/CloudKitService/ModifyZones/CloudKitServiceTests.ModifyZones+Helpers.swift b/Tests/MistKitTests/CloudKitService/ModifyZones/CloudKitServiceTests.ModifyZones+Helpers.swift index fe0deefb..5329f9fd 100644 --- a/Tests/MistKitTests/CloudKitService/ModifyZones/CloudKitServiceTests.ModifyZones+Helpers.swift +++ b/Tests/MistKitTests/CloudKitService/ModifyZones/CloudKitServiceTests.ModifyZones+Helpers.swift @@ -27,9 +27,9 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import HTTPTypes -import Testing +internal import Foundation +internal import HTTPTypes +internal import Testing @testable import MistKit diff --git a/Tests/MistKitTests/CloudKitService/ModifyZones/CloudKitServiceTests.ModifyZones+SuccessCases.swift b/Tests/MistKitTests/CloudKitService/ModifyZones/CloudKitServiceTests.ModifyZones+SuccessCases.swift index 1f14bcb5..eaae52bd 100644 --- a/Tests/MistKitTests/CloudKitService/ModifyZones/CloudKitServiceTests.ModifyZones+SuccessCases.swift +++ b/Tests/MistKitTests/CloudKitService/ModifyZones/CloudKitServiceTests.ModifyZones+SuccessCases.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistKit diff --git a/Tests/MistKitTests/CloudKitService/ModifyZones/CloudKitServiceTests.ModifyZones.swift b/Tests/MistKitTests/CloudKitService/ModifyZones/CloudKitServiceTests.ModifyZones.swift index 93763dcc..3a0efda8 100644 --- a/Tests/MistKitTests/CloudKitService/ModifyZones/CloudKitServiceTests.ModifyZones.swift +++ b/Tests/MistKitTests/CloudKitService/ModifyZones/CloudKitServiceTests.ModifyZones.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistKit diff --git a/Tests/MistKitTests/CloudKitService/Query/CloudKitServiceTests.Query+Configuration.swift b/Tests/MistKitTests/CloudKitService/Query/CloudKitServiceTests.Query+Configuration.swift index 0a73f68a..7c3d8f07 100644 --- a/Tests/MistKitTests/CloudKitService/Query/CloudKitServiceTests.Query+Configuration.swift +++ b/Tests/MistKitTests/CloudKitService/Query/CloudKitServiceTests.Query+Configuration.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistKit diff --git a/Tests/MistKitTests/CloudKitService/Query/CloudKitServiceTests.Query+EdgeCases.swift b/Tests/MistKitTests/CloudKitService/Query/CloudKitServiceTests.Query+EdgeCases.swift index 754162b2..619b75ad 100644 --- a/Tests/MistKitTests/CloudKitService/Query/CloudKitServiceTests.Query+EdgeCases.swift +++ b/Tests/MistKitTests/CloudKitService/Query/CloudKitServiceTests.Query+EdgeCases.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistKit diff --git a/Tests/MistKitTests/CloudKitService/Query/CloudKitServiceTests.Query+ExistingRecordNames.swift b/Tests/MistKitTests/CloudKitService/Query/CloudKitServiceTests.Query+ExistingRecordNames.swift index aa33a5c6..d84dcb72 100644 --- a/Tests/MistKitTests/CloudKitService/Query/CloudKitServiceTests.Query+ExistingRecordNames.swift +++ b/Tests/MistKitTests/CloudKitService/Query/CloudKitServiceTests.Query+ExistingRecordNames.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistKit diff --git a/Tests/MistKitTests/CloudKitService/Query/CloudKitServiceTests.Query+FilterConversion.swift b/Tests/MistKitTests/CloudKitService/Query/CloudKitServiceTests.Query+FilterConversion.swift index 08d6268d..ec0ac91f 100644 --- a/Tests/MistKitTests/CloudKitService/Query/CloudKitServiceTests.Query+FilterConversion.swift +++ b/Tests/MistKitTests/CloudKitService/Query/CloudKitServiceTests.Query+FilterConversion.swift @@ -27,9 +27,9 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation +internal import Foundation internal import MistKitOpenAPI -import Testing +internal import Testing @testable import MistKit diff --git a/Tests/MistKitTests/CloudKitService/Query/CloudKitServiceTests.Query+Helpers.swift b/Tests/MistKitTests/CloudKitService/Query/CloudKitServiceTests.Query+Helpers.swift index 47a76773..60b92d97 100644 --- a/Tests/MistKitTests/CloudKitService/Query/CloudKitServiceTests.Query+Helpers.swift +++ b/Tests/MistKitTests/CloudKitService/Query/CloudKitServiceTests.Query+Helpers.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistKit diff --git a/Tests/MistKitTests/CloudKitService/Query/CloudKitServiceTests.Query+SortConversion.swift b/Tests/MistKitTests/CloudKitService/Query/CloudKitServiceTests.Query+SortConversion.swift index 19bfcb7b..2cf00553 100644 --- a/Tests/MistKitTests/CloudKitService/Query/CloudKitServiceTests.Query+SortConversion.swift +++ b/Tests/MistKitTests/CloudKitService/Query/CloudKitServiceTests.Query+SortConversion.swift @@ -27,9 +27,9 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation +internal import Foundation internal import MistKitOpenAPI -import Testing +internal import Testing @testable import MistKit diff --git a/Tests/MistKitTests/CloudKitService/Query/CloudKitServiceTests.Query.swift b/Tests/MistKitTests/CloudKitService/Query/CloudKitServiceTests.Query.swift index 18e5cd5a..14e9ff02 100644 --- a/Tests/MistKitTests/CloudKitService/Query/CloudKitServiceTests.Query.swift +++ b/Tests/MistKitTests/CloudKitService/Query/CloudKitServiceTests.Query.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistKit diff --git a/Tests/MistKitTests/CloudKitService/QueryPagination/CloudKitServiceTests.QueryPagination+ErrorCases.swift b/Tests/MistKitTests/CloudKitService/QueryPagination/CloudKitServiceTests.QueryPagination+ErrorCases.swift index 34f16333..99369308 100644 --- a/Tests/MistKitTests/CloudKitService/QueryPagination/CloudKitServiceTests.QueryPagination+ErrorCases.swift +++ b/Tests/MistKitTests/CloudKitService/QueryPagination/CloudKitServiceTests.QueryPagination+ErrorCases.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistKit diff --git a/Tests/MistKitTests/CloudKitService/QueryPagination/CloudKitServiceTests.QueryPagination+Helpers.swift b/Tests/MistKitTests/CloudKitService/QueryPagination/CloudKitServiceTests.QueryPagination+Helpers.swift index c94ddbe6..2cdbcaa9 100644 --- a/Tests/MistKitTests/CloudKitService/QueryPagination/CloudKitServiceTests.QueryPagination+Helpers.swift +++ b/Tests/MistKitTests/CloudKitService/QueryPagination/CloudKitServiceTests.QueryPagination+Helpers.swift @@ -27,9 +27,9 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import HTTPTypes -import Testing +internal import Foundation +internal import HTTPTypes +internal import Testing @testable import MistKit diff --git a/Tests/MistKitTests/CloudKitService/QueryPagination/CloudKitServiceTests.QueryPagination+SuccessCases.swift b/Tests/MistKitTests/CloudKitService/QueryPagination/CloudKitServiceTests.QueryPagination+SuccessCases.swift index b3d76a48..ff225664 100644 --- a/Tests/MistKitTests/CloudKitService/QueryPagination/CloudKitServiceTests.QueryPagination+SuccessCases.swift +++ b/Tests/MistKitTests/CloudKitService/QueryPagination/CloudKitServiceTests.QueryPagination+SuccessCases.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistKit diff --git a/Tests/MistKitTests/CloudKitService/QueryPagination/CloudKitServiceTests.QueryPagination.swift b/Tests/MistKitTests/CloudKitService/QueryPagination/CloudKitServiceTests.QueryPagination.swift index 217e06e5..a07b178e 100644 --- a/Tests/MistKitTests/CloudKitService/QueryPagination/CloudKitServiceTests.QueryPagination.swift +++ b/Tests/MistKitTests/CloudKitService/QueryPagination/CloudKitServiceTests.QueryPagination.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistKit diff --git a/Tests/MistKitTests/CloudKitService/Upload/CloudKitServiceTests.Upload+ErrorHandling.swift b/Tests/MistKitTests/CloudKitService/Upload/CloudKitServiceTests.Upload+ErrorHandling.swift index 807a410a..1c013390 100644 --- a/Tests/MistKitTests/CloudKitService/Upload/CloudKitServiceTests.Upload+ErrorHandling.swift +++ b/Tests/MistKitTests/CloudKitService/Upload/CloudKitServiceTests.Upload+ErrorHandling.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistKit diff --git a/Tests/MistKitTests/CloudKitService/Upload/CloudKitServiceTests.Upload+Helpers.swift b/Tests/MistKitTests/CloudKitService/Upload/CloudKitServiceTests.Upload+Helpers.swift index 3591f4b8..11b098c7 100644 --- a/Tests/MistKitTests/CloudKitService/Upload/CloudKitServiceTests.Upload+Helpers.swift +++ b/Tests/MistKitTests/CloudKitService/Upload/CloudKitServiceTests.Upload+Helpers.swift @@ -27,9 +27,9 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import HTTPTypes -import Testing +internal import Foundation +internal import HTTPTypes +internal import Testing @testable import MistKit diff --git a/Tests/MistKitTests/CloudKitService/Upload/CloudKitServiceTests.Upload+NetworkErrors.swift b/Tests/MistKitTests/CloudKitService/Upload/CloudKitServiceTests.Upload+NetworkErrors.swift index f770bf4a..933a57ea 100644 --- a/Tests/MistKitTests/CloudKitService/Upload/CloudKitServiceTests.Upload+NetworkErrors.swift +++ b/Tests/MistKitTests/CloudKitService/Upload/CloudKitServiceTests.Upload+NetworkErrors.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistKit diff --git a/Tests/MistKitTests/CloudKitService/Upload/CloudKitServiceTests.Upload+SuccessCases.swift b/Tests/MistKitTests/CloudKitService/Upload/CloudKitServiceTests.Upload+SuccessCases.swift index 1752a0cd..e7c30692 100644 --- a/Tests/MistKitTests/CloudKitService/Upload/CloudKitServiceTests.Upload+SuccessCases.swift +++ b/Tests/MistKitTests/CloudKitService/Upload/CloudKitServiceTests.Upload+SuccessCases.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistKit diff --git a/Tests/MistKitTests/CloudKitService/Upload/CloudKitServiceTests.Upload+Validation.swift b/Tests/MistKitTests/CloudKitService/Upload/CloudKitServiceTests.Upload+Validation.swift index 5d58b446..3e9e22dd 100644 --- a/Tests/MistKitTests/CloudKitService/Upload/CloudKitServiceTests.Upload+Validation.swift +++ b/Tests/MistKitTests/CloudKitService/Upload/CloudKitServiceTests.Upload+Validation.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistKit diff --git a/Tests/MistKitTests/CloudKitService/Upload/CloudKitServiceTests.Upload.swift b/Tests/MistKitTests/CloudKitService/Upload/CloudKitServiceTests.Upload.swift index 4038bd08..1bae43fe 100644 --- a/Tests/MistKitTests/CloudKitService/Upload/CloudKitServiceTests.Upload.swift +++ b/Tests/MistKitTests/CloudKitService/Upload/CloudKitServiceTests.Upload.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistKit diff --git a/Tests/MistKitTests/Extensions/RecordOperationConversionTests.swift b/Tests/MistKitTests/Extensions/RecordOperationConversionTests.swift index ca9ab28a..95bade16 100644 --- a/Tests/MistKitTests/Extensions/RecordOperationConversionTests.swift +++ b/Tests/MistKitTests/Extensions/RecordOperationConversionTests.swift @@ -27,9 +27,9 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation +internal import Foundation internal import MistKitOpenAPI -import Testing +internal import Testing @testable import MistKit diff --git a/Tests/MistKitTests/Extensions/RegexPatterns/RegexPatternsTests+Convenience.swift b/Tests/MistKitTests/Extensions/RegexPatterns/RegexPatternsTests+Convenience.swift index adf28f9b..d5773656 100644 --- a/Tests/MistKitTests/Extensions/RegexPatterns/RegexPatternsTests+Convenience.swift +++ b/Tests/MistKitTests/Extensions/RegexPatterns/RegexPatternsTests+Convenience.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistKit diff --git a/Tests/MistKitTests/Extensions/RegexPatterns/RegexPatternsTests+Validation.swift b/Tests/MistKitTests/Extensions/RegexPatterns/RegexPatternsTests+Validation.swift index bc31f5b4..f5d44e82 100644 --- a/Tests/MistKitTests/Extensions/RegexPatterns/RegexPatternsTests+Validation.swift +++ b/Tests/MistKitTests/Extensions/RegexPatterns/RegexPatternsTests+Validation.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistKit diff --git a/Tests/MistKitTests/Extensions/RegexPatterns/RegexPatternsTests.swift b/Tests/MistKitTests/Extensions/RegexPatterns/RegexPatternsTests.swift index 9f7995ae..3441f8ad 100644 --- a/Tests/MistKitTests/Extensions/RegexPatterns/RegexPatternsTests.swift +++ b/Tests/MistKitTests/Extensions/RegexPatterns/RegexPatternsTests.swift @@ -27,7 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Testing +internal import Testing @Suite("Regex Patterns") internal enum RegexPatternsTests {} diff --git a/Tests/MistKitTests/Helpers/Platform.swift b/Tests/MistKitTests/Helpers/Platform.swift index 282535a4..d64b75b0 100644 --- a/Tests/MistKitTests/Helpers/Platform.swift +++ b/Tests/MistKitTests/Helpers/Platform.swift @@ -1,5 +1,5 @@ -import Foundation -import Testing +internal import Foundation +internal import Testing /// Platform detection utilities for testing internal enum Platform { diff --git a/Tests/MistKitTests/Mocks/MockTransport.swift b/Tests/MistKitTests/Mocks/MockTransport.swift index a8731985..e6e03d4d 100644 --- a/Tests/MistKitTests/Mocks/MockTransport.swift +++ b/Tests/MistKitTests/Mocks/MockTransport.swift @@ -27,9 +27,9 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import HTTPTypes -import OpenAPIRuntime +internal import Foundation +internal import HTTPTypes +internal import OpenAPIRuntime /// Mock transport for testing that doesn't make actual network calls internal struct MockTransport: ClientTransport, Sendable { diff --git a/Tests/MistKitTests/Mocks/ResponseConfig.swift b/Tests/MistKitTests/Mocks/ResponseConfig.swift index b27dc047..935494fb 100644 --- a/Tests/MistKitTests/Mocks/ResponseConfig.swift +++ b/Tests/MistKitTests/Mocks/ResponseConfig.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import HTTPTypes +internal import Foundation +internal import HTTPTypes /// Configuration for a mock HTTP response internal struct ResponseConfig: Sendable { diff --git a/Tests/MistKitTests/Mocks/ResponseProvider.swift b/Tests/MistKitTests/Mocks/ResponseProvider.swift index c6f9c88f..7d7bf6d5 100644 --- a/Tests/MistKitTests/Mocks/ResponseProvider.swift +++ b/Tests/MistKitTests/Mocks/ResponseProvider.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import HTTPTypes -import OpenAPIRuntime +internal import HTTPTypes +internal import OpenAPIRuntime /// Thread-safe provider for configuring mock responses internal actor ResponseProvider { diff --git a/Tests/MistKitTests/Mocks/TokenManagers/MockTokenManagerWithConnectionError.swift b/Tests/MistKitTests/Mocks/TokenManagers/MockTokenManagerWithConnectionError.swift index 2043cba6..ca15daba 100644 --- a/Tests/MistKitTests/Mocks/TokenManagers/MockTokenManagerWithConnectionError.swift +++ b/Tests/MistKitTests/Mocks/TokenManagers/MockTokenManagerWithConnectionError.swift @@ -4,8 +4,8 @@ // // Created by Leo Dion on 9/24/25. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistKit diff --git a/Tests/MistKitTests/Mocks/TokenManagers/MockTokenManagerWithIntermittentFailures.swift b/Tests/MistKitTests/Mocks/TokenManagers/MockTokenManagerWithIntermittentFailures.swift index 004df7ed..44224c00 100644 --- a/Tests/MistKitTests/Mocks/TokenManagers/MockTokenManagerWithIntermittentFailures.swift +++ b/Tests/MistKitTests/Mocks/TokenManagers/MockTokenManagerWithIntermittentFailures.swift @@ -5,8 +5,8 @@ // Created by Leo Dion on 9/24/25. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistKit diff --git a/Tests/MistKitTests/Mocks/TokenManagers/MockTokenManagerWithRateLimiting.swift b/Tests/MistKitTests/Mocks/TokenManagers/MockTokenManagerWithRateLimiting.swift index 57f43b8e..1933b057 100644 --- a/Tests/MistKitTests/Mocks/TokenManagers/MockTokenManagerWithRateLimiting.swift +++ b/Tests/MistKitTests/Mocks/TokenManagers/MockTokenManagerWithRateLimiting.swift @@ -4,10 +4,10 @@ // // Created by Leo Dion on 9/24/25. // -import Foundation -import HTTPTypes -import OpenAPIRuntime -import Testing +internal import Foundation +internal import HTTPTypes +internal import OpenAPIRuntime +internal import Testing @testable import MistKit diff --git a/Tests/MistKitTests/Mocks/TokenManagers/MockTokenManagerWithRecovery.swift b/Tests/MistKitTests/Mocks/TokenManagers/MockTokenManagerWithRecovery.swift index 57e9d542..06d5f4cc 100644 --- a/Tests/MistKitTests/Mocks/TokenManagers/MockTokenManagerWithRecovery.swift +++ b/Tests/MistKitTests/Mocks/TokenManagers/MockTokenManagerWithRecovery.swift @@ -5,8 +5,8 @@ // Created by Leo Dion on 9/24/25. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistKit diff --git a/Tests/MistKitTests/Mocks/TokenManagers/MockTokenManagerWithRefresh.swift b/Tests/MistKitTests/Mocks/TokenManagers/MockTokenManagerWithRefresh.swift index 6f69fc51..56377e93 100644 --- a/Tests/MistKitTests/Mocks/TokenManagers/MockTokenManagerWithRefresh.swift +++ b/Tests/MistKitTests/Mocks/TokenManagers/MockTokenManagerWithRefresh.swift @@ -5,10 +5,10 @@ // Created by Leo Dion on 9/24/25. // -import Foundation -import HTTPTypes -import OpenAPIRuntime -import Testing +internal import Foundation +internal import HTTPTypes +internal import OpenAPIRuntime +internal import Testing @testable import MistKit diff --git a/Tests/MistKitTests/Mocks/TokenManagers/MockTokenManagerWithRefreshFailure.swift b/Tests/MistKitTests/Mocks/TokenManagers/MockTokenManagerWithRefreshFailure.swift index c134cfd8..0cb2421f 100644 --- a/Tests/MistKitTests/Mocks/TokenManagers/MockTokenManagerWithRefreshFailure.swift +++ b/Tests/MistKitTests/Mocks/TokenManagers/MockTokenManagerWithRefreshFailure.swift @@ -5,8 +5,8 @@ // Created by Leo Dion on 9/24/25. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistKit diff --git a/Tests/MistKitTests/Mocks/TokenManagers/MockTokenManagerWithRefreshTimeout.swift b/Tests/MistKitTests/Mocks/TokenManagers/MockTokenManagerWithRefreshTimeout.swift index 7b825438..19e15dde 100644 --- a/Tests/MistKitTests/Mocks/TokenManagers/MockTokenManagerWithRefreshTimeout.swift +++ b/Tests/MistKitTests/Mocks/TokenManagers/MockTokenManagerWithRefreshTimeout.swift @@ -1,7 +1,7 @@ -import Foundation -import HTTPTypes -import OpenAPIRuntime -import Testing +internal import Foundation +internal import HTTPTypes +internal import OpenAPIRuntime +internal import Testing @testable import MistKit diff --git a/Tests/MistKitTests/Mocks/TokenManagers/MockTokenManagerWithRetry.swift b/Tests/MistKitTests/Mocks/TokenManagers/MockTokenManagerWithRetry.swift index ed475caa..9c456475 100644 --- a/Tests/MistKitTests/Mocks/TokenManagers/MockTokenManagerWithRetry.swift +++ b/Tests/MistKitTests/Mocks/TokenManagers/MockTokenManagerWithRetry.swift @@ -5,8 +5,8 @@ // Created by Leo Dion on 9/24/25. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistKit diff --git a/Tests/MistKitTests/Mocks/TokenManagers/MockTokenManagerWithTimeout.swift b/Tests/MistKitTests/Mocks/TokenManagers/MockTokenManagerWithTimeout.swift index 86e975b1..67ab8569 100644 --- a/Tests/MistKitTests/Mocks/TokenManagers/MockTokenManagerWithTimeout.swift +++ b/Tests/MistKitTests/Mocks/TokenManagers/MockTokenManagerWithTimeout.swift @@ -5,8 +5,8 @@ // Created by Leo Dion on 9/24/25. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistKit diff --git a/Tests/MistKitTests/Models/AssetUploading/AssetUploadTokenTests.swift b/Tests/MistKitTests/Models/AssetUploading/AssetUploadTokenTests.swift index 02a60177..7cb70ea0 100644 --- a/Tests/MistKitTests/Models/AssetUploading/AssetUploadTokenTests.swift +++ b/Tests/MistKitTests/Models/AssetUploading/AssetUploadTokenTests.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistKit diff --git a/Tests/MistKitTests/Models/BatchSyncResultTests.swift b/Tests/MistKitTests/Models/BatchSyncResultTests.swift index 13b9607a..b6bc241d 100644 --- a/Tests/MistKitTests/Models/BatchSyncResultTests.swift +++ b/Tests/MistKitTests/Models/BatchSyncResultTests.swift @@ -27,9 +27,9 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation +internal import Foundation internal import MistKitOpenAPI -import Testing +internal import Testing @testable import MistKit diff --git a/Tests/MistKitTests/Models/DatabaseTests.swift b/Tests/MistKitTests/Models/DatabaseTests.swift index 679290f5..8bc9fff2 100644 --- a/Tests/MistKitTests/Models/DatabaseTests.swift +++ b/Tests/MistKitTests/Models/DatabaseTests.swift @@ -1,5 +1,5 @@ -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistKit diff --git a/Tests/MistKitTests/Models/EnvironmentTests.swift b/Tests/MistKitTests/Models/EnvironmentTests.swift index 6973cb57..bb0497b7 100644 --- a/Tests/MistKitTests/Models/EnvironmentTests.swift +++ b/Tests/MistKitTests/Models/EnvironmentTests.swift @@ -1,5 +1,5 @@ -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistKit diff --git a/Tests/MistKitTests/Models/FieldValues/FieldValueConversionTests+BasicTypes.swift b/Tests/MistKitTests/Models/FieldValues/FieldValueConversionTests+BasicTypes.swift index d8007f2f..bb95d1e0 100644 --- a/Tests/MistKitTests/Models/FieldValues/FieldValueConversionTests+BasicTypes.swift +++ b/Tests/MistKitTests/Models/FieldValues/FieldValueConversionTests+BasicTypes.swift @@ -1,6 +1,6 @@ -import Foundation +internal import Foundation internal import MistKitOpenAPI -import Testing +internal import Testing @testable import MistKit diff --git a/Tests/MistKitTests/Models/FieldValues/FieldValueConversionTests+ComplexTypes.swift b/Tests/MistKitTests/Models/FieldValues/FieldValueConversionTests+ComplexTypes.swift index d58d3d92..4832779e 100644 --- a/Tests/MistKitTests/Models/FieldValues/FieldValueConversionTests+ComplexTypes.swift +++ b/Tests/MistKitTests/Models/FieldValues/FieldValueConversionTests+ComplexTypes.swift @@ -1,6 +1,6 @@ -import Foundation +internal import Foundation internal import MistKitOpenAPI -import Testing +internal import Testing @testable import MistKit diff --git a/Tests/MistKitTests/Models/FieldValues/FieldValueConversionTests+EdgeCases.swift b/Tests/MistKitTests/Models/FieldValues/FieldValueConversionTests+EdgeCases.swift index e454294a..3ca45c80 100644 --- a/Tests/MistKitTests/Models/FieldValues/FieldValueConversionTests+EdgeCases.swift +++ b/Tests/MistKitTests/Models/FieldValues/FieldValueConversionTests+EdgeCases.swift @@ -1,6 +1,6 @@ -import Foundation +internal import Foundation internal import MistKitOpenAPI -import Testing +internal import Testing @testable import MistKit diff --git a/Tests/MistKitTests/Models/FieldValues/FieldValueConversionTests+Lists.swift b/Tests/MistKitTests/Models/FieldValues/FieldValueConversionTests+Lists.swift index 9fa87df7..a3c7d567 100644 --- a/Tests/MistKitTests/Models/FieldValues/FieldValueConversionTests+Lists.swift +++ b/Tests/MistKitTests/Models/FieldValues/FieldValueConversionTests+Lists.swift @@ -1,6 +1,6 @@ -import Foundation +internal import Foundation internal import MistKitOpenAPI -import Testing +internal import Testing @testable import MistKit diff --git a/Tests/MistKitTests/Models/FieldValues/FieldValueConversionTests.swift b/Tests/MistKitTests/Models/FieldValues/FieldValueConversionTests.swift index 49eb5b16..52128bfc 100644 --- a/Tests/MistKitTests/Models/FieldValues/FieldValueConversionTests.swift +++ b/Tests/MistKitTests/Models/FieldValues/FieldValueConversionTests.swift @@ -1,5 +1,5 @@ -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistKit diff --git a/Tests/MistKitTests/Models/FieldValues/FieldValueTests.swift b/Tests/MistKitTests/Models/FieldValues/FieldValueTests.swift index 8b90abc4..2cd16ef2 100644 --- a/Tests/MistKitTests/Models/FieldValues/FieldValueTests.swift +++ b/Tests/MistKitTests/Models/FieldValues/FieldValueTests.swift @@ -1,5 +1,5 @@ -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistKit diff --git a/Tests/MistKitTests/Models/OperationClassificationTests.swift b/Tests/MistKitTests/Models/OperationClassificationTests.swift index 4d0e6aec..49db491d 100644 --- a/Tests/MistKitTests/Models/OperationClassificationTests.swift +++ b/Tests/MistKitTests/Models/OperationClassificationTests.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistKit diff --git a/Tests/MistKitTests/Models/Queries/FilterBuilder/FilterBuilderTests+Comparators.swift b/Tests/MistKitTests/Models/Queries/FilterBuilder/FilterBuilderTests+Comparators.swift index 7999e050..b6010a9d 100644 --- a/Tests/MistKitTests/Models/Queries/FilterBuilder/FilterBuilderTests+Comparators.swift +++ b/Tests/MistKitTests/Models/Queries/FilterBuilder/FilterBuilderTests+Comparators.swift @@ -1,6 +1,6 @@ -import Foundation +internal import Foundation internal import MistKitOpenAPI -import Testing +internal import Testing @testable import MistKit diff --git a/Tests/MistKitTests/Models/Queries/FilterBuilder/FilterBuilderTests+ComplexValues.swift b/Tests/MistKitTests/Models/Queries/FilterBuilder/FilterBuilderTests+ComplexValues.swift index c011d1fd..539bd7ef 100644 --- a/Tests/MistKitTests/Models/Queries/FilterBuilder/FilterBuilderTests+ComplexValues.swift +++ b/Tests/MistKitTests/Models/Queries/FilterBuilder/FilterBuilderTests+ComplexValues.swift @@ -1,6 +1,6 @@ -import Foundation +internal import Foundation internal import MistKitOpenAPI -import Testing +internal import Testing @testable import MistKit diff --git a/Tests/MistKitTests/Models/Queries/FilterBuilder/FilterBuilderTests+ListFilters.swift b/Tests/MistKitTests/Models/Queries/FilterBuilder/FilterBuilderTests+ListFilters.swift index 0111eff9..74683a6e 100644 --- a/Tests/MistKitTests/Models/Queries/FilterBuilder/FilterBuilderTests+ListFilters.swift +++ b/Tests/MistKitTests/Models/Queries/FilterBuilder/FilterBuilderTests+ListFilters.swift @@ -1,6 +1,6 @@ -import Foundation +internal import Foundation internal import MistKitOpenAPI -import Testing +internal import Testing @testable import MistKit diff --git a/Tests/MistKitTests/Models/Queries/FilterBuilder/FilterBuilderTests+StringFilters.swift b/Tests/MistKitTests/Models/Queries/FilterBuilder/FilterBuilderTests+StringFilters.swift index 826651a7..61719a80 100644 --- a/Tests/MistKitTests/Models/Queries/FilterBuilder/FilterBuilderTests+StringFilters.swift +++ b/Tests/MistKitTests/Models/Queries/FilterBuilder/FilterBuilderTests+StringFilters.swift @@ -1,6 +1,6 @@ -import Foundation +internal import Foundation internal import MistKitOpenAPI -import Testing +internal import Testing @testable import MistKit diff --git a/Tests/MistKitTests/Models/Queries/FilterBuilder/FilterBuilderTests.swift b/Tests/MistKitTests/Models/Queries/FilterBuilder/FilterBuilderTests.swift index 0a6d334b..bfd123f9 100644 --- a/Tests/MistKitTests/Models/Queries/FilterBuilder/FilterBuilderTests.swift +++ b/Tests/MistKitTests/Models/Queries/FilterBuilder/FilterBuilderTests.swift @@ -1,4 +1,4 @@ -import Testing +internal import Testing @Suite("Filter Builder", .enabled(if: Platform.isCryptoAvailable)) internal enum FilterBuilderTests {} diff --git a/Tests/MistKitTests/Models/Queries/QueryFilterTests+Comparison.swift b/Tests/MistKitTests/Models/Queries/QueryFilterTests+Comparison.swift index 2e4ce3d7..ec56b399 100644 --- a/Tests/MistKitTests/Models/Queries/QueryFilterTests+Comparison.swift +++ b/Tests/MistKitTests/Models/Queries/QueryFilterTests+Comparison.swift @@ -1,6 +1,6 @@ -import Foundation +internal import Foundation internal import MistKitOpenAPI -import Testing +internal import Testing @testable import MistKit diff --git a/Tests/MistKitTests/Models/Queries/QueryFilterTests+ComplexFields.swift b/Tests/MistKitTests/Models/Queries/QueryFilterTests+ComplexFields.swift index 06dea963..02eb04df 100644 --- a/Tests/MistKitTests/Models/Queries/QueryFilterTests+ComplexFields.swift +++ b/Tests/MistKitTests/Models/Queries/QueryFilterTests+ComplexFields.swift @@ -1,6 +1,6 @@ -import Foundation +internal import Foundation internal import MistKitOpenAPI -import Testing +internal import Testing @testable import MistKit diff --git a/Tests/MistKitTests/Models/Queries/QueryFilterTests+EdgeCases.swift b/Tests/MistKitTests/Models/Queries/QueryFilterTests+EdgeCases.swift index adb9a50b..d89d5697 100644 --- a/Tests/MistKitTests/Models/Queries/QueryFilterTests+EdgeCases.swift +++ b/Tests/MistKitTests/Models/Queries/QueryFilterTests+EdgeCases.swift @@ -1,6 +1,6 @@ -import Foundation +internal import Foundation internal import MistKitOpenAPI -import Testing +internal import Testing @testable import MistKit diff --git a/Tests/MistKitTests/Models/Queries/QueryFilterTests+Equality.swift b/Tests/MistKitTests/Models/Queries/QueryFilterTests+Equality.swift index 4024cca0..930671b2 100644 --- a/Tests/MistKitTests/Models/Queries/QueryFilterTests+Equality.swift +++ b/Tests/MistKitTests/Models/Queries/QueryFilterTests+Equality.swift @@ -1,6 +1,6 @@ -import Foundation +internal import Foundation internal import MistKitOpenAPI -import Testing +internal import Testing @testable import MistKit diff --git a/Tests/MistKitTests/Models/Queries/QueryFilterTests+List.swift b/Tests/MistKitTests/Models/Queries/QueryFilterTests+List.swift index 71732e06..fcdfd835 100644 --- a/Tests/MistKitTests/Models/Queries/QueryFilterTests+List.swift +++ b/Tests/MistKitTests/Models/Queries/QueryFilterTests+List.swift @@ -1,6 +1,6 @@ -import Foundation +internal import Foundation internal import MistKitOpenAPI -import Testing +internal import Testing @testable import MistKit diff --git a/Tests/MistKitTests/Models/Queries/QueryFilterTests+ListMember.swift b/Tests/MistKitTests/Models/Queries/QueryFilterTests+ListMember.swift index 815327b4..a07413e5 100644 --- a/Tests/MistKitTests/Models/Queries/QueryFilterTests+ListMember.swift +++ b/Tests/MistKitTests/Models/Queries/QueryFilterTests+ListMember.swift @@ -1,6 +1,6 @@ -import Foundation +internal import Foundation internal import MistKitOpenAPI -import Testing +internal import Testing @testable import MistKit diff --git a/Tests/MistKitTests/Models/Queries/QueryFilterTests+String.swift b/Tests/MistKitTests/Models/Queries/QueryFilterTests+String.swift index 0877d92d..d99fdd39 100644 --- a/Tests/MistKitTests/Models/Queries/QueryFilterTests+String.swift +++ b/Tests/MistKitTests/Models/Queries/QueryFilterTests+String.swift @@ -1,6 +1,6 @@ -import Foundation +internal import Foundation internal import MistKitOpenAPI -import Testing +internal import Testing @testable import MistKit diff --git a/Tests/MistKitTests/Models/Queries/QueryFilterTests.swift b/Tests/MistKitTests/Models/Queries/QueryFilterTests.swift index 9c60e039..aa8d747b 100644 --- a/Tests/MistKitTests/Models/Queries/QueryFilterTests.swift +++ b/Tests/MistKitTests/Models/Queries/QueryFilterTests.swift @@ -1,5 +1,5 @@ -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistKit diff --git a/Tests/MistKitTests/Models/Queries/QuerySortTests.swift b/Tests/MistKitTests/Models/Queries/QuerySortTests.swift index ab534e62..65fe8e82 100644 --- a/Tests/MistKitTests/Models/Queries/QuerySortTests.swift +++ b/Tests/MistKitTests/Models/Queries/QuerySortTests.swift @@ -1,6 +1,6 @@ -import Foundation +internal import Foundation internal import MistKitOpenAPI -import Testing +internal import Testing @testable import MistKit diff --git a/Tests/MistKitTests/Models/RecordInfoTests.swift b/Tests/MistKitTests/Models/RecordInfoTests.swift index 26f8b898..26423fe3 100644 --- a/Tests/MistKitTests/Models/RecordInfoTests.swift +++ b/Tests/MistKitTests/Models/RecordInfoTests.swift @@ -1,6 +1,6 @@ -import Foundation +internal import Foundation internal import MistKitOpenAPI -import Testing +internal import Testing @testable import MistKit diff --git a/Tests/MistKitTests/OpenAPI/LoggingMiddleware/LoggingMiddlewareTests+Advanced.swift b/Tests/MistKitTests/OpenAPI/LoggingMiddleware/LoggingMiddlewareTests+Advanced.swift index ad0eddbf..01aeb1fe 100644 --- a/Tests/MistKitTests/OpenAPI/LoggingMiddleware/LoggingMiddlewareTests+Advanced.swift +++ b/Tests/MistKitTests/OpenAPI/LoggingMiddleware/LoggingMiddlewareTests+Advanced.swift @@ -27,10 +27,10 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import HTTPTypes -import OpenAPIRuntime -import Testing +internal import Foundation +internal import HTTPTypes +internal import OpenAPIRuntime +internal import Testing @testable import MistKit diff --git a/Tests/MistKitTests/OpenAPI/LoggingMiddleware/LoggingMiddlewareTests+Basic.swift b/Tests/MistKitTests/OpenAPI/LoggingMiddleware/LoggingMiddlewareTests+Basic.swift index ef12959f..53db1b73 100644 --- a/Tests/MistKitTests/OpenAPI/LoggingMiddleware/LoggingMiddlewareTests+Basic.swift +++ b/Tests/MistKitTests/OpenAPI/LoggingMiddleware/LoggingMiddlewareTests+Basic.swift @@ -27,10 +27,10 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import HTTPTypes -import OpenAPIRuntime -import Testing +internal import Foundation +internal import HTTPTypes +internal import OpenAPIRuntime +internal import Testing @testable import MistKit diff --git a/Tests/MistKitTests/OpenAPI/LoggingMiddleware/LoggingMiddlewareTests+StatusTests.swift b/Tests/MistKitTests/OpenAPI/LoggingMiddleware/LoggingMiddlewareTests+StatusTests.swift index 58781d10..9f817251 100644 --- a/Tests/MistKitTests/OpenAPI/LoggingMiddleware/LoggingMiddlewareTests+StatusTests.swift +++ b/Tests/MistKitTests/OpenAPI/LoggingMiddleware/LoggingMiddlewareTests+StatusTests.swift @@ -27,10 +27,10 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import HTTPTypes -import OpenAPIRuntime -import Testing +internal import Foundation +internal import HTTPTypes +internal import OpenAPIRuntime +internal import Testing @testable import MistKit diff --git a/Tests/MistKitTests/OpenAPI/LoggingMiddleware/LoggingMiddlewareTests.swift b/Tests/MistKitTests/OpenAPI/LoggingMiddleware/LoggingMiddlewareTests.swift index bc5ba8f9..22132609 100644 --- a/Tests/MistKitTests/OpenAPI/LoggingMiddleware/LoggingMiddlewareTests.swift +++ b/Tests/MistKitTests/OpenAPI/LoggingMiddleware/LoggingMiddlewareTests.swift @@ -27,7 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Testing +internal import Testing @Suite("Logging Middleware") internal enum LoggingMiddlewareTests {} diff --git a/Tests/MistKitTests/RecordManagement/CloudKitRecordTests+Conformance.swift b/Tests/MistKitTests/RecordManagement/CloudKitRecordTests+Conformance.swift index c7c01599..dc45edef 100644 --- a/Tests/MistKitTests/RecordManagement/CloudKitRecordTests+Conformance.swift +++ b/Tests/MistKitTests/RecordManagement/CloudKitRecordTests+Conformance.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistKit diff --git a/Tests/MistKitTests/RecordManagement/CloudKitRecordTests+FieldConversion.swift b/Tests/MistKitTests/RecordManagement/CloudKitRecordTests+FieldConversion.swift index 007b1a05..e58aa249 100644 --- a/Tests/MistKitTests/RecordManagement/CloudKitRecordTests+FieldConversion.swift +++ b/Tests/MistKitTests/RecordManagement/CloudKitRecordTests+FieldConversion.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistKit diff --git a/Tests/MistKitTests/RecordManagement/CloudKitRecordTests+Formatting.swift b/Tests/MistKitTests/RecordManagement/CloudKitRecordTests+Formatting.swift index dc7f2c01..9850cc27 100644 --- a/Tests/MistKitTests/RecordManagement/CloudKitRecordTests+Formatting.swift +++ b/Tests/MistKitTests/RecordManagement/CloudKitRecordTests+Formatting.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistKit diff --git a/Tests/MistKitTests/RecordManagement/CloudKitRecordTests+Parsing.swift b/Tests/MistKitTests/RecordManagement/CloudKitRecordTests+Parsing.swift index 7ee5fb73..b3392962 100644 --- a/Tests/MistKitTests/RecordManagement/CloudKitRecordTests+Parsing.swift +++ b/Tests/MistKitTests/RecordManagement/CloudKitRecordTests+Parsing.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistKit diff --git a/Tests/MistKitTests/RecordManagement/CloudKitRecordTests+RoundTrip.swift b/Tests/MistKitTests/RecordManagement/CloudKitRecordTests+RoundTrip.swift index 8a97c890..bda5bee6 100644 --- a/Tests/MistKitTests/RecordManagement/CloudKitRecordTests+RoundTrip.swift +++ b/Tests/MistKitTests/RecordManagement/CloudKitRecordTests+RoundTrip.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistKit diff --git a/Tests/MistKitTests/RecordManagement/CloudKitRecordTests.swift b/Tests/MistKitTests/RecordManagement/CloudKitRecordTests.swift index 33500670..a7bb4577 100644 --- a/Tests/MistKitTests/RecordManagement/CloudKitRecordTests.swift +++ b/Tests/MistKitTests/RecordManagement/CloudKitRecordTests.swift @@ -27,7 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Testing +internal import Testing @testable import MistKit diff --git a/Tests/MistKitTests/RecordManagement/FieldValueConvenienceTests.swift b/Tests/MistKitTests/RecordManagement/FieldValueConvenienceTests.swift index 66b9b10b..2f7945d1 100644 --- a/Tests/MistKitTests/RecordManagement/FieldValueConvenienceTests.swift +++ b/Tests/MistKitTests/RecordManagement/FieldValueConvenienceTests.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistKit diff --git a/Tests/MistKitTests/RecordManagement/MockRecordManagingService.swift b/Tests/MistKitTests/RecordManagement/MockRecordManagingService.swift index 32888dc0..da711077 100644 --- a/Tests/MistKitTests/RecordManagement/MockRecordManagingService.swift +++ b/Tests/MistKitTests/RecordManagement/MockRecordManagingService.swift @@ -27,7 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation +internal import Foundation @testable import MistKit diff --git a/Tests/MistKitTests/RecordManagement/RecordManagingTests+List.swift b/Tests/MistKitTests/RecordManagement/RecordManagingTests+List.swift index 20e91ca8..d6ed6fec 100644 --- a/Tests/MistKitTests/RecordManagement/RecordManagingTests+List.swift +++ b/Tests/MistKitTests/RecordManagement/RecordManagingTests+List.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistKit diff --git a/Tests/MistKitTests/RecordManagement/RecordManagingTests+Query.swift b/Tests/MistKitTests/RecordManagement/RecordManagingTests+Query.swift index 06489b21..fd80d850 100644 --- a/Tests/MistKitTests/RecordManagement/RecordManagingTests+Query.swift +++ b/Tests/MistKitTests/RecordManagement/RecordManagingTests+Query.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistKit diff --git a/Tests/MistKitTests/RecordManagement/RecordManagingTests+Sync.swift b/Tests/MistKitTests/RecordManagement/RecordManagingTests+Sync.swift index c00f2aa8..15c087fa 100644 --- a/Tests/MistKitTests/RecordManagement/RecordManagingTests+Sync.swift +++ b/Tests/MistKitTests/RecordManagement/RecordManagingTests+Sync.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation -import Testing +internal import Foundation +internal import Testing @testable import MistKit diff --git a/Tests/MistKitTests/RecordManagement/RecordManagingTests.swift b/Tests/MistKitTests/RecordManagement/RecordManagingTests.swift index fe950202..0f62f0f4 100644 --- a/Tests/MistKitTests/RecordManagement/RecordManagingTests.swift +++ b/Tests/MistKitTests/RecordManagement/RecordManagingTests.swift @@ -27,7 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Testing +internal import Testing @testable import MistKit diff --git a/Tests/MistKitTests/RecordManagement/TestRecord.swift b/Tests/MistKitTests/RecordManagement/TestRecord.swift index ddec1dd7..7892f967 100644 --- a/Tests/MistKitTests/RecordManagement/TestRecord.swift +++ b/Tests/MistKitTests/RecordManagement/TestRecord.swift @@ -27,7 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import Foundation +internal import Foundation @testable import MistKit From 78462780f220a8c5bc2d6cdd33d23f42f5eb45c4 Mon Sep 17 00:00:00 2001 From: Leo Dion Date: Tue, 19 May 2026 11:05:31 +0100 Subject: [PATCH 05/35] New Rebase --- .swiftlint.yml | 1 + .../.github/actions/cloudkit-sync/action.yml | 18 +- .../.github/workflows/BushelCloud.yml | 18 +- .../.github/workflows/bushel-cloud-build.yml | 7 +- .../BushelCloud/.github/workflows/codeql.yml | 9 +- Examples/BushelCloud/.gitrepo | 4 +- Examples/BushelCloud/Package.resolved | 2 +- Examples/BushelCloud/Package.swift | 16 +- .../Configuration/ConfigKey+BUSHEL.swift | 60 ++++ .../Sources/ConfigKeyKit/ConfigKey.swift | 181 ----------- .../ConfigKeyKit/ConfigurationKey.swift | 84 ----- .../ConfigKeyKit/OptionalConfigKey.swift | 117 ------- .../ConfigKeySourceTests.swift | 21 -- .../ConfigKeyKitTests/ConfigKeyTests.swift | 58 ---- .../ConfigKeyKitTests/NamingStyleTests.swift | 37 --- .../OptionalConfigKeyTests.swift | 62 ---- .../.github/workflows/CelestraCloud.yml | 2 +- .../.github/workflows/codeql.yml | 4 +- .../.github/workflows/update-feeds.yml | 2 +- Examples/CelestraCloud/.gitrepo | 4 +- Examples/MistDemo/Package.resolved | 2 +- Examples/MistDemo/Package.swift | 12 +- .../actions/setup-configkeykit/action.yml | 26 ++ .../.github/actions/setup-tools/action.yml | 29 ++ .../.github/workflows/ConfigKeyKit.yml | 288 ++++++++++++++++++ .../.github/workflows/check-unsafe-flags.yml | 39 +++ .../.github/workflows/claude-code-review.yml | 54 ++++ .../ConfigKeyKit/.github/workflows/claude.yml | 50 +++ .../.github/workflows/cleanup-caches.yml | 29 ++ .../ConfigKeyKit/.github/workflows/codeql.yml | 82 +++++ .../.github/workflows/swift-source-compat.yml | 29 ++ Packages/ConfigKeyKit/.gitignore | 84 +++++ Packages/ConfigKeyKit/.gitrepo | 12 + Packages/ConfigKeyKit/.periphery.yml | 3 + Packages/ConfigKeyKit/.spi.yml | 5 + Packages/ConfigKeyKit/.swift-format | 70 +++++ Packages/ConfigKeyKit/.swift-version | 1 + Packages/ConfigKeyKit/.swiftlint.yml | 141 +++++++++ Packages/ConfigKeyKit/LICENSE | 21 ++ Packages/ConfigKeyKit/Makefile | 22 ++ Packages/ConfigKeyKit/Package.swift | 47 +++ Packages/ConfigKeyKit/README.md | 106 +++++++ Packages/ConfigKeyKit/Scripts/header.sh | 113 +++++++ Packages/ConfigKeyKit/Scripts/lint.sh | 67 ++++ .../Sources/ConfigKeyKit/Command.swift | 4 +- .../ConfigKeyKit/CommandConfiguration.swift | 4 +- .../ConfigKeyKit/CommandLineParser.swift | 4 +- .../ConfigKeyKit/CommandRegistry.swift | 4 +- .../ConfigKeyKit/CommandRegistryError.swift | 2 +- .../Sources/ConfigKeyKit/ConfigKey+Bool.swift | 14 +- .../ConfigKeyKit/ConfigKey+Debug.swift | 2 +- .../Sources/ConfigKeyKit/ConfigKey.swift | 10 +- .../ConfigKeyKit/ConfigKeySource.swift | 2 +- .../ConfigKeyKit/ConfigurationKey.swift | 6 +- .../ConfigKeyKit/ConfigurationParseable.swift | 5 +- .../Sources/ConfigKeyKit/NamingStyle.swift | 2 +- .../OptionalConfigKey+Debug.swift | 2 +- .../ConfigKeyKit/OptionalConfigKey.swift | 8 +- .../ConfigKeyKit/StandardNamingStyle.swift | 4 +- .../CommandLineParserTests.swift | 12 +- .../CommandRegistry+AvailableCommands.swift | 23 +- .../CommandRegistry+CommandCreation.swift | 17 +- ...CommandRegistry+CommandTypeRetrieval.swift | 15 +- .../CommandRegistry+ConcurrentAccess.swift | 23 +- .../CommandRegistry+Errors.swift | 6 +- .../CommandRegistry+Metadata.swift | 15 +- .../CommandRegistry+Overwrite.swift | 15 +- .../CommandRegistry+Registration.swift | 21 +- .../CommandRegistry+TestCommandTypes.swift | 10 +- .../CommandRegistry/CommandRegistry.swift | 14 +- .../ConfigKeySourceTests.swift | 43 +++ .../ConfigKeyKitTests/ConfigKeyTests.swift | 83 +++++ .../ConfigKeyKitTests/NamingStyleTests.swift | 59 ++++ .../OptionalConfigKeyTests.swift | 84 +++++ .../ConfigKeyKitTests/TestEnvironment.swift | 16 + Packages/ConfigKeyKit/codecov.yml | 9 + Packages/ConfigKeyKit/mise.toml | 7 + .../APITokenAuthenticator.swift | 4 +- .../ServerToServerAuthenticator.swift | 4 +- .../WebAuthTokenAuthenticator.swift | 4 +- .../CloudKitError+OpenAPI.swift | 30 +- .../CloudKitService+AssetUpload.swift | 28 +- .../CloudKitService+WriteOperations.swift | 45 ++- .../CloudKitService/CloudKitService.swift | 1 + .../Extensions/JSONDecoder+Shared.swift | 37 +++ .../Extensions/JSONEncoder+Shared.swift | 37 +++ .../Models/RecordOperation+EncodedSize.swift | 6 +- ...oudKitServiceTests.SizeLimits+Assets.swift | 4 +- 88 files changed, 1991 insertions(+), 793 deletions(-) create mode 100644 Examples/BushelCloud/Sources/BushelCloudKit/Configuration/ConfigKey+BUSHEL.swift delete mode 100644 Examples/BushelCloud/Sources/ConfigKeyKit/ConfigKey.swift delete mode 100644 Examples/BushelCloud/Sources/ConfigKeyKit/ConfigurationKey.swift delete mode 100644 Examples/BushelCloud/Sources/ConfigKeyKit/OptionalConfigKey.swift delete mode 100644 Examples/BushelCloud/Tests/ConfigKeyKitTests/ConfigKeySourceTests.swift delete mode 100644 Examples/BushelCloud/Tests/ConfigKeyKitTests/ConfigKeyTests.swift delete mode 100644 Examples/BushelCloud/Tests/ConfigKeyKitTests/NamingStyleTests.swift delete mode 100644 Examples/BushelCloud/Tests/ConfigKeyKitTests/OptionalConfigKeyTests.swift create mode 100644 Packages/ConfigKeyKit/.github/actions/setup-configkeykit/action.yml create mode 100644 Packages/ConfigKeyKit/.github/actions/setup-tools/action.yml create mode 100644 Packages/ConfigKeyKit/.github/workflows/ConfigKeyKit.yml create mode 100644 Packages/ConfigKeyKit/.github/workflows/check-unsafe-flags.yml create mode 100644 Packages/ConfigKeyKit/.github/workflows/claude-code-review.yml create mode 100644 Packages/ConfigKeyKit/.github/workflows/claude.yml create mode 100644 Packages/ConfigKeyKit/.github/workflows/cleanup-caches.yml create mode 100644 Packages/ConfigKeyKit/.github/workflows/codeql.yml create mode 100644 Packages/ConfigKeyKit/.github/workflows/swift-source-compat.yml create mode 100644 Packages/ConfigKeyKit/.gitignore create mode 100644 Packages/ConfigKeyKit/.gitrepo create mode 100644 Packages/ConfigKeyKit/.periphery.yml create mode 100644 Packages/ConfigKeyKit/.spi.yml create mode 100644 Packages/ConfigKeyKit/.swift-format create mode 100644 Packages/ConfigKeyKit/.swift-version create mode 100644 Packages/ConfigKeyKit/.swiftlint.yml create mode 100644 Packages/ConfigKeyKit/LICENSE create mode 100644 Packages/ConfigKeyKit/Makefile create mode 100644 Packages/ConfigKeyKit/Package.swift create mode 100644 Packages/ConfigKeyKit/README.md create mode 100755 Packages/ConfigKeyKit/Scripts/header.sh create mode 100755 Packages/ConfigKeyKit/Scripts/lint.sh rename {Examples/MistDemo => Packages/ConfigKeyKit}/Sources/ConfigKeyKit/Command.swift (97%) rename {Examples/MistDemo => Packages/ConfigKeyKit}/Sources/ConfigKeyKit/CommandConfiguration.swift (96%) rename {Examples/MistDemo => Packages/ConfigKeyKit}/Sources/ConfigKeyKit/CommandLineParser.swift (97%) rename {Examples/MistDemo => Packages/ConfigKeyKit}/Sources/ConfigKeyKit/CommandRegistry.swift (98%) rename {Examples/MistDemo => Packages/ConfigKeyKit}/Sources/ConfigKeyKit/CommandRegistryError.swift (98%) rename {Examples/MistDemo => Packages/ConfigKeyKit}/Sources/ConfigKeyKit/ConfigKey+Bool.swift (86%) rename {Examples/MistDemo => Packages/ConfigKeyKit}/Sources/ConfigKeyKit/ConfigKey+Debug.swift (98%) rename {Examples/MistDemo => Packages/ConfigKeyKit}/Sources/ConfigKeyKit/ConfigKey.swift (95%) rename {Examples/MistDemo => Packages/ConfigKeyKit}/Sources/ConfigKeyKit/ConfigKeySource.swift (98%) rename {Examples/MistDemo => Packages/ConfigKeyKit}/Sources/ConfigKeyKit/ConfigurationKey.swift (95%) rename {Examples/MistDemo => Packages/ConfigKeyKit}/Sources/ConfigKeyKit/ConfigurationParseable.swift (95%) rename {Examples/MistDemo => Packages/ConfigKeyKit}/Sources/ConfigKeyKit/NamingStyle.swift (98%) rename {Examples/MistDemo => Packages/ConfigKeyKit}/Sources/ConfigKeyKit/OptionalConfigKey+Debug.swift (98%) rename {Examples/MistDemo => Packages/ConfigKeyKit}/Sources/ConfigKeyKit/OptionalConfigKey.swift (96%) rename {Examples/MistDemo => Packages/ConfigKeyKit}/Sources/ConfigKeyKit/StandardNamingStyle.swift (94%) rename {Examples/MistDemo/Tests/MistDemoTests/ConfigKeyKit => Packages/ConfigKeyKit/Tests/ConfigKeyKitTests}/CommandLineParserTests.swift (85%) rename Examples/MistDemo/Tests/MistDemoTests/ConfigKeyKit/CommandRegistry/CommandRegistryTests+AvailableCommands.swift => Packages/ConfigKeyKit/Tests/ConfigKeyKitTests/CommandRegistry/CommandRegistry+AvailableCommands.swift (78%) rename Examples/MistDemo/Tests/MistDemoTests/ConfigKeyKit/CommandRegistry/CommandRegistryTests+CommandCreation.swift => Packages/ConfigKeyKit/Tests/ConfigKeyKitTests/CommandRegistry/CommandRegistry+CommandCreation.swift (82%) rename Examples/MistDemo/Tests/MistDemoTests/ConfigKeyKit/CommandRegistry/CommandRegistryTests+CommandTypeRetrieval.swift => Packages/ConfigKeyKit/Tests/ConfigKeyKitTests/CommandRegistry/CommandRegistry+CommandTypeRetrieval.swift (85%) rename Examples/MistDemo/Tests/MistDemoTests/ConfigKeyKit/CommandRegistry/CommandRegistryTests+ConcurrentAccess.swift => Packages/ConfigKeyKit/Tests/ConfigKeyKitTests/CommandRegistry/CommandRegistry+ConcurrentAccess.swift (81%) rename Examples/MistDemo/Tests/MistDemoTests/ConfigKeyKit/CommandRegistry/CommandRegistryTests+Errors.swift => Packages/ConfigKeyKit/Tests/ConfigKeyKitTests/CommandRegistry/CommandRegistry+Errors.swift (95%) rename Examples/MistDemo/Tests/MistDemoTests/ConfigKeyKit/CommandRegistry/CommandRegistryTests+Metadata.swift => Packages/ConfigKeyKit/Tests/ConfigKeyKitTests/CommandRegistry/CommandRegistry+Metadata.swift (86%) rename Examples/MistDemo/Tests/MistDemoTests/ConfigKeyKit/CommandRegistry/CommandRegistryTests+Overwrite.swift => Packages/ConfigKeyKit/Tests/ConfigKeyKitTests/CommandRegistry/CommandRegistry+Overwrite.swift (82%) rename Examples/MistDemo/Tests/MistDemoTests/ConfigKeyKit/CommandRegistry/CommandRegistryTests+Registration.swift => Packages/ConfigKeyKit/Tests/ConfigKeyKitTests/CommandRegistry/CommandRegistry+Registration.swift (80%) rename Examples/MistDemo/Tests/MistDemoTests/ConfigKeyKit/CommandRegistry/CommandRegistryTests+TestCommandTypes.swift => Packages/ConfigKeyKit/Tests/ConfigKeyKitTests/CommandRegistry/CommandRegistry+TestCommandTypes.swift (94%) rename Examples/MistDemo/Tests/MistDemoTests/ConfigKeyKit/CommandRegistry/CommandRegistryTests.swift => Packages/ConfigKeyKit/Tests/ConfigKeyKitTests/CommandRegistry/CommandRegistry.swift (82%) create mode 100644 Packages/ConfigKeyKit/Tests/ConfigKeyKitTests/ConfigKeySourceTests.swift create mode 100644 Packages/ConfigKeyKit/Tests/ConfigKeyKitTests/ConfigKeyTests.swift create mode 100644 Packages/ConfigKeyKit/Tests/ConfigKeyKitTests/NamingStyleTests.swift create mode 100644 Packages/ConfigKeyKit/Tests/ConfigKeyKitTests/OptionalConfigKeyTests.swift create mode 100644 Packages/ConfigKeyKit/Tests/ConfigKeyKitTests/TestEnvironment.swift create mode 100644 Packages/ConfigKeyKit/codecov.yml create mode 100644 Packages/ConfigKeyKit/mise.toml create mode 100644 Sources/MistKit/Extensions/JSONDecoder+Shared.swift create mode 100644 Sources/MistKit/Extensions/JSONEncoder+Shared.swift diff --git a/.swiftlint.yml b/.swiftlint.yml index 7f5af019..e70f8bb9 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -122,6 +122,7 @@ excluded: - .build - Mint - Examples + - Packages - Sources/MistKitOpenAPI - Package.swift indentation_width: diff --git a/Examples/BushelCloud/.github/actions/cloudkit-sync/action.yml b/Examples/BushelCloud/.github/actions/cloudkit-sync/action.yml index 3d41f1f8..bcfcd26b 100644 --- a/Examples/BushelCloud/.github/actions/cloudkit-sync/action.yml +++ b/Examples/BushelCloud/.github/actions/cloudkit-sync/action.yml @@ -130,6 +130,14 @@ inputs: description: 'Run export after sync and generate reports' required: false default: 'true' + mistkit-branch: + description: 'MistKit ref to check out when falling back to a fresh build' + required: false + default: 'v1.0.0-beta.2' + configkeykit-branch: + description: 'ConfigKeyKit ref to check out when falling back to a fresh build' + required: false + default: 'main' runs: using: "composite" @@ -147,7 +155,15 @@ runs: - name: Setup MistKit if: steps.download-binary.outcome != 'success' - uses: ./.github/actions/setup-mistkit + uses: brightdigit/MistKit/.github/actions/setup-mistkit@main + with: + branch: ${{ inputs.mistkit-branch }} + + - name: Setup ConfigKeyKit + if: steps.download-binary.outcome != 'success' + uses: brightdigit/ConfigKeyKit/.github/actions/setup-configkeykit@main + with: + branch: ${{ inputs.configkeykit-branch }} - name: Build binary (fallback if artifact unavailable) if: steps.download-binary.outcome != 'success' diff --git a/Examples/BushelCloud/.github/workflows/BushelCloud.yml b/Examples/BushelCloud/.github/workflows/BushelCloud.yml index f83f5a53..2d5732aa 100644 --- a/Examples/BushelCloud/.github/workflows/BushelCloud.yml +++ b/Examples/BushelCloud/.github/workflows/BushelCloud.yml @@ -21,7 +21,8 @@ concurrency: env: PACKAGE_NAME: BushelCloud - MISTKIT_BRANCH: v1.0.0-beta.1 + MISTKIT_BRANCH: v1.0.0-beta.2 + CONFIGKEYKIT_BRANCH: main jobs: configure: @@ -89,6 +90,11 @@ jobs: with: branch: ${{ env.MISTKIT_BRANCH }} + - name: Setup ConfigKeyKit + uses: brightdigit/ConfigKeyKit/.github/actions/setup-configkeykit@main + with: + branch: ${{ env.CONFIGKEYKIT_BRANCH }} + - uses: brightdigit/swift-build@v1 id: build with: @@ -178,6 +184,11 @@ jobs: with: branch: ${{ env.MISTKIT_BRANCH }} + - name: Setup ConfigKeyKit + uses: brightdigit/ConfigKeyKit/.github/actions/setup-configkeykit@main + with: + branch: ${{ env.CONFIGKEYKIT_BRANCH }} + - name: Build and Test id: build uses: brightdigit/swift-build@v1 @@ -243,6 +254,11 @@ jobs: with: branch: ${{ env.MISTKIT_BRANCH }} + - name: Setup ConfigKeyKit + uses: brightdigit/ConfigKeyKit/.github/actions/setup-configkeykit@main + with: + branch: ${{ env.CONFIGKEYKIT_BRANCH }} + - name: Build and Test id: build uses: brightdigit/swift-build@v1 diff --git a/Examples/BushelCloud/.github/workflows/bushel-cloud-build.yml b/Examples/BushelCloud/.github/workflows/bushel-cloud-build.yml index 8a0a13d5..aed2ca72 100644 --- a/Examples/BushelCloud/.github/workflows/bushel-cloud-build.yml +++ b/Examples/BushelCloud/.github/workflows/bushel-cloud-build.yml @@ -40,7 +40,12 @@ jobs: - name: Setup MistKit uses: brightdigit/MistKit/.github/actions/setup-mistkit@main with: - branch: v1.0.0-beta.1 + branch: v1.0.0-beta.2 + + - name: Setup ConfigKeyKit + uses: brightdigit/ConfigKeyKit/.github/actions/setup-configkeykit@main + with: + branch: main - name: Verify Swift version run: | diff --git a/Examples/BushelCloud/.github/workflows/codeql.yml b/Examples/BushelCloud/.github/workflows/codeql.yml index a653cb18..624ed00b 100644 --- a/Examples/BushelCloud/.github/workflows/codeql.yml +++ b/Examples/BushelCloud/.github/workflows/codeql.yml @@ -71,7 +71,14 @@ jobs: - name: Setup MistKit - uses: ./.github/actions/setup-mistkit + uses: brightdigit/MistKit/.github/actions/setup-mistkit@main + with: + branch: v1.0.0-beta.2 + + - name: Setup ConfigKeyKit + uses: brightdigit/ConfigKeyKit/.github/actions/setup-configkeykit@main + with: + branch: main # Autobuild attempts to build any compiled languages (C/C++, C#, Go, Java, or Swift). # If this step fails, then you should remove it and run the build manually (see below) diff --git a/Examples/BushelCloud/.gitrepo b/Examples/BushelCloud/.gitrepo index 9c3dc537..26600d51 100644 --- a/Examples/BushelCloud/.gitrepo +++ b/Examples/BushelCloud/.gitrepo @@ -6,7 +6,7 @@ [subrepo] remote = git@github.com:brightdigit/BushelCloud.git branch = mistkit - commit = 5bb449083cf63d4752dea48fe5579efc16ba7374 - parent = 38f0d77f93f1df4384271be2ff865cae2e2f813d + commit = 66b595eb2e9d3a12a385edaae4a0e549f9d48da5 + parent = c31250a988eede3e8523ac6b97096ec2c91e99b2 method = merge cmdver = 0.4.9 diff --git a/Examples/BushelCloud/Package.resolved b/Examples/BushelCloud/Package.resolved index e0b877de..740e5324 100644 --- a/Examples/BushelCloud/Package.resolved +++ b/Examples/BushelCloud/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "c3ac1cf77d89f143a19ef295fe93dc532ed8453816f62104a1d89923205611da", + "originHash" : "19206e85a58e39bd539ec38237e3cc167902ae05697e150f556c029593646dbe", "pins" : [ { "identity" : "bushelkit", diff --git a/Examples/BushelCloud/Package.swift b/Examples/BushelCloud/Package.swift index 00cfd538..f9dc5fbe 100644 --- a/Examples/BushelCloud/Package.swift +++ b/Examples/BushelCloud/Package.swift @@ -87,12 +87,12 @@ let package = Package( .visionOS(.v2) ], products: [ - .library(name: "ConfigKeyKit", targets: ["ConfigKeyKit"]), .library(name: "BushelCloudKit", targets: ["BushelCloudKit"]), .executable(name: "bushel-cloud", targets: ["BushelCloudCLI"]) ], dependencies: [ .package(name: "MistKit", path: "../.."), + .package(name: "ConfigKeyKit", path: "../../Packages/ConfigKeyKit"), .package(url: "https://github.com/brightdigit/BushelKit.git", from: "3.0.0-alpha.2"), .package(url: "https://github.com/brightdigit/IPSWDownloads.git", from: "1.0.0"), .package(url: "https://github.com/scinfu/SwiftSoup.git", from: "2.6.0"), @@ -103,15 +103,10 @@ let package = Package( ) ], targets: [ - .target( - name: "ConfigKeyKit", - dependencies: [], - swiftSettings: swiftSettings - ), .target( name: "BushelCloudKit", dependencies: [ - .target(name: "ConfigKeyKit"), + .product(name: "ConfigKeyKit", package: "ConfigKeyKit"), .product(name: "MistKit", package: "MistKit"), .product(name: "BushelLogging", package: "BushelKit"), .product(name: "BushelFoundation", package: "BushelKit"), @@ -130,13 +125,6 @@ let package = Package( ], swiftSettings: swiftSettings ), - .testTarget( - name: "ConfigKeyKitTests", - dependencies: [ - .target(name: "ConfigKeyKit") - ], - swiftSettings: swiftSettings - ), .testTarget( name: "BushelCloudKitTests", dependencies: [ diff --git a/Examples/BushelCloud/Sources/BushelCloudKit/Configuration/ConfigKey+BUSHEL.swift b/Examples/BushelCloud/Sources/BushelCloudKit/Configuration/ConfigKey+BUSHEL.swift new file mode 100644 index 00000000..9b71aa17 --- /dev/null +++ b/Examples/BushelCloud/Sources/BushelCloudKit/Configuration/ConfigKey+BUSHEL.swift @@ -0,0 +1,60 @@ +// +// ConfigKey+BUSHEL.swift +// BushelCloud +// +// Created by Leo Dion. +// Copyright © 2026 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +public import ConfigKeyKit + +// MARK: - BushelCloud-Specific Config Key Helpers + +extension ConfigKey { + /// Convenience initializer for keys with `BUSHEL_` environment-variable prefix. + /// - Parameters: + /// - base: Base key string (e.g., "sync.dry_run") + /// - defaultVal: Required default value + public init(bushelPrefixed base: String, default defaultVal: Value) { + self.init(base, envPrefix: "BUSHEL", default: defaultVal) + } +} + +extension ConfigKey where Value == Bool { + /// Convenience initializer for boolean keys with `BUSHEL_` environment-variable prefix. + /// - Parameters: + /// - base: Base key string (e.g., "sync.verbose") + /// - defaultVal: Default value (defaults to false) + public init(bushelPrefixed base: String, default defaultVal: Bool = false) { + self.init(base, envPrefix: "BUSHEL", default: defaultVal) + } +} + +extension OptionalConfigKey { + /// Convenience initializer for optional keys with `BUSHEL_` environment-variable prefix. + /// - Parameter base: Base key string (e.g., "sync.min_interval") + public init(bushelPrefixed base: String) { + self.init(base, envPrefix: "BUSHEL") + } +} diff --git a/Examples/BushelCloud/Sources/ConfigKeyKit/ConfigKey.swift b/Examples/BushelCloud/Sources/ConfigKeyKit/ConfigKey.swift deleted file mode 100644 index b497316d..00000000 --- a/Examples/BushelCloud/Sources/ConfigKeyKit/ConfigKey.swift +++ /dev/null @@ -1,181 +0,0 @@ -// -// ConfigKey.swift -// BushelCloud -// -// Created by Leo Dion. -// Copyright © 2026 BrightDigit. -// -// Permission is hereby granted, free of charge, to any person -// obtaining a copy of this software and associated documentation -// files (the "Software"), to deal in the Software without -// restriction, including without limitation the rights to use, -// copy, modify, merge, publish, distribute, sublicense, and/or -// sell copies of the Software, and to permit persons to whom the -// Software is furnished to do so, subject to the following -// conditions: -// -// The above copyright notice and this permission notice shall be -// included in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES -// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT -// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -// OTHER DEALINGS IN THE SOFTWARE. -// - -internal import Foundation - -// MARK: - Generic Configuration Key - -/// Configuration key for values with default fallbacks -/// -/// Use `ConfigKey` when a configuration value has a sensible default -/// that should be used when not provided by the user. The `read()` method -/// will always return a non-optional value. -/// -/// Example: -/// ```swift -/// let containerID = ConfigKey( -/// base: "cloudkit.container_id", -/// default: "iCloud.com.brightdigit.Bushel" -/// ) -/// // read(containerID) returns String (non-optional) -/// ``` -public struct ConfigKey: ConfigurationKey, Sendable { - private let baseKey: String? - private let styles: [ConfigKeySource: any NamingStyle] - private let explicitKeys: [ConfigKeySource: String] - public let defaultValue: Value // Non-optional! - - /// Initialize with explicit CLI and ENV keys and required default - public init(cli: String? = nil, env: String? = nil, default defaultVal: Value) { - self.baseKey = nil - self.styles = [:] - var keys: [ConfigKeySource: String] = [:] - if let cli = cli { keys[.commandLine] = cli } - if let env = env { keys[.environment] = env } - self.explicitKeys = keys - self.defaultValue = defaultVal - } - - /// Initialize from a base key string with naming styles and required default - /// - Parameters: - /// - base: Base key string (e.g., "cloudkit.container_id") - /// - styles: Dictionary mapping sources to naming styles - /// - defaultVal: Required default value - public init( - base: String, - styles: [ConfigKeySource: any NamingStyle], - default defaultVal: Value - ) { - self.baseKey = base - self.styles = styles - self.explicitKeys = [:] - self.defaultValue = defaultVal - } - - /// Convenience initializer with standard naming conventions and required default - /// - Parameters: - /// - base: Base key string (e.g., "cloudkit.container_id") - /// - envPrefix: Prefix for environment variable (defaults to nil) - /// - defaultVal: Required default value - public init(_ base: String, envPrefix: String? = nil, default defaultVal: Value) { - self.baseKey = base - self.styles = [ - .commandLine: StandardNamingStyle.dotSeparated, - .environment: StandardNamingStyle.screamingSnakeCase(prefix: envPrefix), - ] - self.explicitKeys = [:] - self.defaultValue = defaultVal - } - - public func key(for source: ConfigKeySource) -> String? { - // Check for explicit key first - if let explicit = explicitKeys[source] { - return explicit - } - - // Generate from base key and style - guard let base = baseKey, let style = styles[source] else { - return nil - } - - return style.transform(base) - } -} - -extension ConfigKey: CustomDebugStringConvertible { - public var debugDescription: String { - let cliKey = key(for: .commandLine) ?? "nil" - let envKey = key(for: .environment) ?? "nil" - return "ConfigKey(cli: \(cliKey), env: \(envKey), default: \(defaultValue))" - } -} - -// MARK: - Convenience Initializers for BUSHEL Prefix - -extension ConfigKey { - /// Convenience initializer for keys with BUSHEL prefix - /// - Parameters: - /// - base: Base key string (e.g., "sync.dry_run") - /// - defaultVal: Required default value - public init(bushelPrefixed base: String, default defaultVal: Value) { - self.init(base, envPrefix: "BUSHEL", default: defaultVal) - } -} - -// MARK: - Specialized Initializers for Booleans - -extension ConfigKey where Value == Bool { - /// Non-optional default value accessor for booleans - @available(*, deprecated, message: "Use defaultValue directly instead") - public var boolDefault: Bool { - defaultValue // Already non-optional! - } - - /// Initialize a boolean configuration key with non-optional default - /// - Parameters: - /// - cli: Command-line argument name - /// - env: Environment variable name - /// - defaultVal: Default value (defaults to false) - public init(cli: String, env: String, default defaultVal: Bool = false) { - self.baseKey = nil - self.styles = [:] - var keys: [ConfigKeySource: String] = [:] - keys[.commandLine] = cli - keys[.environment] = env - self.explicitKeys = keys - self.defaultValue = defaultVal - } - - /// Initialize a boolean configuration key from base string - /// - Parameters: - /// - base: Base key string (e.g., "sync.verbose") - /// - envPrefix: Prefix for environment variable (defaults to nil) - /// - defaultVal: Default value (defaults to false) - public init(_ base: String, envPrefix: String? = nil, default defaultVal: Bool = false) { - self.baseKey = base - self.styles = [ - .commandLine: StandardNamingStyle.dotSeparated, - .environment: StandardNamingStyle.screamingSnakeCase(prefix: envPrefix), - ] - self.explicitKeys = [:] - self.defaultValue = defaultVal - } -} - -// MARK: - BUSHEL Prefix Convenience - -extension ConfigKey where Value == Bool { - /// Convenience initializer for boolean keys with BUSHEL prefix - /// - Parameters: - /// - base: Base key string (e.g., "sync.verbose") - /// - defaultVal: Default value (defaults to false) - public init(bushelPrefixed base: String, default defaultVal: Bool = false) { - self.init(base, envPrefix: "BUSHEL", default: defaultVal) - } -} diff --git a/Examples/BushelCloud/Sources/ConfigKeyKit/ConfigurationKey.swift b/Examples/BushelCloud/Sources/ConfigKeyKit/ConfigurationKey.swift deleted file mode 100644 index 28a3e51e..00000000 --- a/Examples/BushelCloud/Sources/ConfigKeyKit/ConfigurationKey.swift +++ /dev/null @@ -1,84 +0,0 @@ -// -// ConfigurationKey.swift -// BushelCloud -// -// Created by Leo Dion. -// Copyright © 2026 BrightDigit. -// -// Permission is hereby granted, free of charge, to any person -// obtaining a copy of this software and associated documentation -// files (the "Software"), to deal in the Software without -// restriction, including without limitation the rights to use, -// copy, modify, merge, publish, distribute, sublicense, and/or -// sell copies of the Software, and to permit persons to whom the -// Software is furnished to do so, subject to the following -// conditions: -// -// The above copyright notice and this permission notice shall be -// included in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES -// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT -// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -// OTHER DEALINGS IN THE SOFTWARE. -// - -internal import Foundation - -// MARK: - Configuration Key Source - -/// Source for configuration keys (CLI arguments or environment variables) -public enum ConfigKeySource: CaseIterable, Sendable { - /// Command-line arguments (e.g., --cloudkit-container-id) - case commandLine - - /// Environment variables (e.g., CLOUDKIT_CONTAINER_ID) - case environment -} - -// MARK: - Naming Style - -/// Protocol for transforming base key strings into different naming conventions -public protocol NamingStyle: Sendable { - /// Transform a base key string according to this naming style - /// - Parameter base: Base key string (e.g., "cloudkit.container_id") - /// - Returns: Transformed key string - func transform(_ base: String) -> String -} - -/// Common naming styles for configuration keys -public enum StandardNamingStyle: NamingStyle, Sendable { - /// Dot-separated lowercase (e.g., "cloudkit.container_id") - case dotSeparated - - /// Screaming snake case with prefix (e.g., "BUSHEL_CLOUDKIT_CONTAINER_ID") - case screamingSnakeCase(prefix: String?) - - public func transform(_ base: String) -> String { - switch self { - case .dotSeparated: - return base - - case .screamingSnakeCase(let prefix): - let snakeCase = base.uppercased().replacingOccurrences(of: ".", with: "_") - if let prefix = prefix { - return "\(prefix)_\(snakeCase)" - } - return snakeCase - } - } -} - -// MARK: - Configuration Key Protocol - -/// Protocol for configuration keys that support multiple sources -public protocol ConfigurationKey: Sendable { - /// Get the key string for a specific source - /// - Parameter source: The configuration source (CLI or ENV) - /// - Returns: The key string for that source, or nil if the key doesn't support that source - func key(for source: ConfigKeySource) -> String? -} diff --git a/Examples/BushelCloud/Sources/ConfigKeyKit/OptionalConfigKey.swift b/Examples/BushelCloud/Sources/ConfigKeyKit/OptionalConfigKey.swift deleted file mode 100644 index 5b818e50..00000000 --- a/Examples/BushelCloud/Sources/ConfigKeyKit/OptionalConfigKey.swift +++ /dev/null @@ -1,117 +0,0 @@ -// -// OptionalConfigKey.swift -// BushelCloud -// -// Created by Leo Dion. -// Copyright © 2026 BrightDigit. -// -// Permission is hereby granted, free of charge, to any person -// obtaining a copy of this software and associated documentation -// files (the "Software"), to deal in the Software without -// restriction, including without limitation the rights to use, -// copy, modify, merge, publish, distribute, sublicense, and/or -// sell copies of the Software, and to permit persons to whom the -// Software is furnished to do so, subject to the following -// conditions: -// -// The above copyright notice and this permission notice shall be -// included in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES -// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT -// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -// OTHER DEALINGS IN THE SOFTWARE. -// - -internal import Foundation - -// MARK: - Optional Configuration Key - -/// Configuration key for optional values without defaults -/// -/// Use `OptionalConfigKey` when a configuration value has no sensible default -/// and should be `nil` when not provided by the user. The `read()` method -/// will return an optional value. -/// -/// Example: -/// ```swift -/// let apiKey = OptionalConfigKey(base: "api.key") -/// // read(apiKey) returns String? -/// ``` -public struct OptionalConfigKey: ConfigurationKey, Sendable { - private let baseKey: String? - private let styles: [ConfigKeySource: any NamingStyle] - private let explicitKeys: [ConfigKeySource: String] - - /// Initialize with explicit CLI and ENV keys (no default) - public init(cli: String? = nil, env: String? = nil) { - self.baseKey = nil - self.styles = [:] - var keys: [ConfigKeySource: String] = [:] - if let cli = cli { keys[.commandLine] = cli } - if let env = env { keys[.environment] = env } - self.explicitKeys = keys - } - - /// Initialize from a base key string with naming styles (no default) - /// - Parameters: - /// - base: Base key string (e.g., "cloudkit.key_id") - /// - styles: Dictionary mapping sources to naming styles - public init( - base: String, - styles: [ConfigKeySource: any NamingStyle] - ) { - self.baseKey = base - self.styles = styles - self.explicitKeys = [:] - } - - /// Convenience initializer with standard naming conventions (no default) - /// - Parameters: - /// - base: Base key string (e.g., "cloudkit.key_id") - /// - envPrefix: Prefix for environment variable (defaults to nil) - public init(_ base: String, envPrefix: String? = nil) { - self.baseKey = base - self.styles = [ - .commandLine: StandardNamingStyle.dotSeparated, - .environment: StandardNamingStyle.screamingSnakeCase(prefix: envPrefix), - ] - self.explicitKeys = [:] - } - - public func key(for source: ConfigKeySource) -> String? { - // Check for explicit key first - if let explicit = explicitKeys[source] { - return explicit - } - - // Generate from base key and style - guard let base = baseKey, let style = styles[source] else { - return nil - } - - return style.transform(base) - } -} - -extension OptionalConfigKey: CustomDebugStringConvertible { - public var debugDescription: String { - let cliKey = key(for: .commandLine) ?? "nil" - let envKey = key(for: .environment) ?? "nil" - return "OptionalConfigKey(cli: \(cliKey), env: \(envKey))" - } -} - -// MARK: - Convenience Initializers for BUSHEL Prefix - -extension OptionalConfigKey { - /// Convenience initializer for keys with BUSHEL prefix - /// - Parameter base: Base key string (e.g., "sync.min_interval") - public init(bushelPrefixed base: String) { - self.init(base, envPrefix: "BUSHEL") - } -} diff --git a/Examples/BushelCloud/Tests/ConfigKeyKitTests/ConfigKeySourceTests.swift b/Examples/BushelCloud/Tests/ConfigKeyKitTests/ConfigKeySourceTests.swift deleted file mode 100644 index ce45cdea..00000000 --- a/Examples/BushelCloud/Tests/ConfigKeyKitTests/ConfigKeySourceTests.swift +++ /dev/null @@ -1,21 +0,0 @@ -// -// ConfigKeySourceTests.swift -// ConfigKeyKit -// -// Tests for ConfigKeySource enum -// - -internal import Testing - -@testable import ConfigKeyKit - -@Suite("ConfigKeySource Tests") -internal struct ConfigKeySourceTests { - @Test("All cases") - internal func allCases() { - let sources = ConfigKeySource.allCases - #expect(sources.count == 2) - #expect(sources.contains(.commandLine)) - #expect(sources.contains(.environment)) - } -} diff --git a/Examples/BushelCloud/Tests/ConfigKeyKitTests/ConfigKeyTests.swift b/Examples/BushelCloud/Tests/ConfigKeyKitTests/ConfigKeyTests.swift deleted file mode 100644 index 3c13267d..00000000 --- a/Examples/BushelCloud/Tests/ConfigKeyKitTests/ConfigKeyTests.swift +++ /dev/null @@ -1,58 +0,0 @@ -// -// ConfigKeyTests.swift -// ConfigKeyKit -// -// Tests for ConfigKey configuration -// - -internal import Testing - -@testable import ConfigKeyKit - -@Suite("ConfigKey Tests") -internal struct ConfigKeyTests { - @Test("ConfigKey with explicit keys and default") - internal func explicitKeys() { - let key = ConfigKey(cli: "test.key", env: "TEST_KEY", default: "default-value") - - #expect(key.key(for: .commandLine) == "test.key") - #expect(key.key(for: .environment) == "TEST_KEY") - #expect(key.defaultValue == "default-value") - } - - @Test("ConfigKey with base string and default prefix") - internal func baseStringWithDefaultPrefix() { - let key = ConfigKey( - bushelPrefixed: "cloudkit.container_id", default: "iCloud.com.example.App" - ) - - #expect(key.key(for: .commandLine) == "cloudkit.container_id") - #expect(key.key(for: .environment) == "BUSHEL_CLOUDKIT_CONTAINER_ID") - #expect(key.defaultValue == "iCloud.com.example.App") - } - - @Test("ConfigKey with base string and no prefix") - internal func baseStringNoPrefix() { - let key = ConfigKey( - "cloudkit.container_id", envPrefix: nil, default: "iCloud.com.example.App" - ) - - #expect(key.key(for: .commandLine) == "cloudkit.container_id") - #expect(key.key(for: .environment) == "CLOUDKIT_CONTAINER_ID") - #expect(key.defaultValue == "iCloud.com.example.App") - } - - @Test("ConfigKey with default value") - internal func defaultValue() { - let key = ConfigKey(cli: "test.key", env: "TEST_KEY", default: "default-value") - - #expect(key.defaultValue == "default-value") - } - - @Test("Boolean ConfigKey with default") - internal func booleanDefaultValue() { - let key = ConfigKey(bushelPrefixed: "sync.verbose", default: false) - - #expect(key.defaultValue == false) - } -} diff --git a/Examples/BushelCloud/Tests/ConfigKeyKitTests/NamingStyleTests.swift b/Examples/BushelCloud/Tests/ConfigKeyKitTests/NamingStyleTests.swift deleted file mode 100644 index 001a65d7..00000000 --- a/Examples/BushelCloud/Tests/ConfigKeyKitTests/NamingStyleTests.swift +++ /dev/null @@ -1,37 +0,0 @@ -// -// NamingStyleTests.swift -// ConfigKeyKit -// -// Tests for naming style transformations -// - -internal import Testing - -@testable import ConfigKeyKit - -@Suite("NamingStyle Tests") -internal struct NamingStyleTests { - @Test("Dot-separated style") - internal func dotSeparatedStyle() { - let style = StandardNamingStyle.dotSeparated - #expect(style.transform("cloudkit.container_id") == "cloudkit.container_id") - } - - @Test("Screaming snake case with prefix") - internal func screamingSnakeCaseWithPrefix() { - let style = StandardNamingStyle.screamingSnakeCase(prefix: "BUSHEL") - #expect(style.transform("cloudkit.container_id") == "BUSHEL_CLOUDKIT_CONTAINER_ID") - } - - @Test("Screaming snake case without prefix") - internal func screamingSnakeCaseNoPrefix() { - let style = StandardNamingStyle.screamingSnakeCase(prefix: nil) - #expect(style.transform("cloudkit.container_id") == "CLOUDKIT_CONTAINER_ID") - } - - @Test("Screaming snake case with nil prefix") - internal func screamingSnakeCaseNilPrefix() { - let style = StandardNamingStyle.screamingSnakeCase(prefix: nil) - #expect(style.transform("sync.verbose") == "SYNC_VERBOSE") - } -} diff --git a/Examples/BushelCloud/Tests/ConfigKeyKitTests/OptionalConfigKeyTests.swift b/Examples/BushelCloud/Tests/ConfigKeyKitTests/OptionalConfigKeyTests.swift deleted file mode 100644 index 425157b1..00000000 --- a/Examples/BushelCloud/Tests/ConfigKeyKitTests/OptionalConfigKeyTests.swift +++ /dev/null @@ -1,62 +0,0 @@ -// -// OptionalConfigKeyTests.swift -// ConfigKeyKit -// -// Tests for OptionalConfigKey configuration -// - -internal import Testing - -@testable import ConfigKeyKit - -@Suite("OptionalConfigKey Tests") -internal struct OptionalConfigKeyTests { - @Test("OptionalConfigKey with explicit keys") - internal func explicitKeys() { - let key = OptionalConfigKey(cli: "test.key", env: "TEST_KEY") - - #expect(key.key(for: .commandLine) == "test.key") - #expect(key.key(for: .environment) == "TEST_KEY") - } - - @Test("OptionalConfigKey with base string and default prefix") - internal func baseStringWithDefaultPrefix() { - let key = OptionalConfigKey(bushelPrefixed: "cloudkit.key_id") - - #expect(key.key(for: .commandLine) == "cloudkit.key_id") - #expect(key.key(for: .environment) == "BUSHEL_CLOUDKIT_KEY_ID") - } - - @Test("OptionalConfigKey with base string and no prefix") - internal func baseStringNoPrefix() { - let key = OptionalConfigKey("cloudkit.key_id", envPrefix: nil) - - #expect(key.key(for: .commandLine) == "cloudkit.key_id") - #expect(key.key(for: .environment) == "CLOUDKIT_KEY_ID") - } - - @Test("OptionalConfigKey and ConfigKey generate identical keys") - internal func keyGenerationParity() { - let optional = OptionalConfigKey(bushelPrefixed: "test.key") - let withDefault = ConfigKey(bushelPrefixed: "test.key", default: "default") - - #expect(optional.key(for: .commandLine) == withDefault.key(for: .commandLine)) - #expect(optional.key(for: .environment) == withDefault.key(for: .environment)) - } - - @Test("OptionalConfigKey for Int type") - internal func intOptionalKey() { - let key = OptionalConfigKey(bushelPrefixed: "sync.min_interval") - - #expect(key.key(for: .commandLine) == "sync.min_interval") - #expect(key.key(for: .environment) == "BUSHEL_SYNC_MIN_INTERVAL") - } - - @Test("OptionalConfigKey for Double type") - internal func doubleOptionalKey() { - let key = OptionalConfigKey(bushelPrefixed: "fetch.interval_global") - - #expect(key.key(for: .commandLine) == "fetch.interval_global") - #expect(key.key(for: .environment) == "BUSHEL_FETCH_INTERVAL_GLOBAL") - } -} diff --git a/Examples/CelestraCloud/.github/workflows/CelestraCloud.yml b/Examples/CelestraCloud/.github/workflows/CelestraCloud.yml index 023e20de..a878600d 100644 --- a/Examples/CelestraCloud/.github/workflows/CelestraCloud.yml +++ b/Examples/CelestraCloud/.github/workflows/CelestraCloud.yml @@ -21,7 +21,7 @@ concurrency: env: PACKAGE_NAME: CelestraCloud - MISTKIT_BRANCH: v1.0.0-beta.1 + MISTKIT_BRANCH: v1.0.0-beta.2 jobs: configure: diff --git a/Examples/CelestraCloud/.github/workflows/codeql.yml b/Examples/CelestraCloud/.github/workflows/codeql.yml index 341134d6..df1ac023 100644 --- a/Examples/CelestraCloud/.github/workflows/codeql.yml +++ b/Examples/CelestraCloud/.github/workflows/codeql.yml @@ -58,7 +58,9 @@ jobs: swift package --version - name: Setup MistKit - uses: ./.github/actions/setup-mistkit + uses: brightdigit/MistKit/.github/actions/setup-mistkit@main + with: + branch: v1.0.0-beta.2 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL diff --git a/Examples/CelestraCloud/.github/workflows/update-feeds.yml b/Examples/CelestraCloud/.github/workflows/update-feeds.yml index 100a7179..8a44920b 100644 --- a/Examples/CelestraCloud/.github/workflows/update-feeds.yml +++ b/Examples/CelestraCloud/.github/workflows/update-feeds.yml @@ -49,7 +49,7 @@ env: CLOUDKIT_KEY_ID: ${{ secrets.CLOUDKIT_KEY_ID }} CLOUDKIT_ENVIRONMENT: ${{ (github.event_name == 'pull_request' || github.event_name == 'push') && 'development' || github.event.inputs.environment || 'production' }} CLOUDKIT_PRIVATE_KEY_PATH: /tmp/cloudkit_key.pem - MISTKIT_BRANCH: v1.0.0-beta.1 + MISTKIT_BRANCH: v1.0.0-beta.2 jobs: # Determine which tier to run based on schedule or manual input diff --git a/Examples/CelestraCloud/.gitrepo b/Examples/CelestraCloud/.gitrepo index 76b381ba..e16d2783 100644 --- a/Examples/CelestraCloud/.gitrepo +++ b/Examples/CelestraCloud/.gitrepo @@ -6,7 +6,7 @@ [subrepo] remote = git@github.com:brightdigit/CelestraCloud.git branch = mistkit - commit = ea897c34cc0cc63c0a4c35bb99bf819535a47c6e - parent = 38f0d77f93f1df4384271be2ff865cae2e2f813d + commit = d91df88dcfe6b8c7cccd2d8257edb0472059ac2f + parent = 3e7a61518aaffa14c259c38087bf8ca75bf080cf method = merge cmdver = 0.4.9 diff --git a/Examples/MistDemo/Package.resolved b/Examples/MistDemo/Package.resolved index 2fa36330..0a714070 100644 --- a/Examples/MistDemo/Package.resolved +++ b/Examples/MistDemo/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "7284c3deec21f39c02edfa30e7214ff910bbb668d02643c0e02f07ab3341122d", + "originHash" : "74809d363120c26bf126107d8453c0c07f761ce0be02bd2e5df3cc4c3b3ced84", "pins" : [ { "identity" : "async-http-client", diff --git a/Examples/MistDemo/Package.swift b/Examples/MistDemo/Package.swift index ab997e79..f2e35720 100644 --- a/Examples/MistDemo/Package.swift +++ b/Examples/MistDemo/Package.swift @@ -106,6 +106,7 @@ let package = Package( ], dependencies: [ .package(name: "MistKit", path: "../.."), + .package(name: "ConfigKeyKit", path: "../../Packages/ConfigKeyKit"), .package( url: "https://github.com/hummingbird-project/hummingbird.git", from: "2.0.0" @@ -125,11 +126,6 @@ let package = Package( ), ], targets: [ - .target( - name: "ConfigKeyKit", - dependencies: [], - swiftSettings: swiftSettings - ), .target( name: "MistDemoApp", dependencies: ["MistDemoKit"], @@ -138,7 +134,7 @@ let package = Package( .target( name: "MistDemoKit", dependencies: [ - "ConfigKeyKit", + .product(name: "ConfigKeyKit", package: "ConfigKeyKit"), .product(name: "MistKit", package: "MistKit"), .product( name: "Hummingbird", @@ -167,7 +163,7 @@ let package = Package( name: "MistDemo", dependencies: [ "MistDemoKit", - "ConfigKeyKit", + .product(name: "ConfigKeyKit", package: "ConfigKeyKit"), .product(name: "MistKit", package: "MistKit"), ], swiftSettings: swiftSettings @@ -176,7 +172,7 @@ let package = Package( name: "MistDemoTests", dependencies: [ "MistDemoKit", - "ConfigKeyKit", + .product(name: "ConfigKeyKit", package: "ConfigKeyKit"), .product(name: "MistKit", package: "MistKit"), .product(name: "MistKitOpenAPI", package: "MistKit"), .product( diff --git a/Packages/ConfigKeyKit/.github/actions/setup-configkeykit/action.yml b/Packages/ConfigKeyKit/.github/actions/setup-configkeykit/action.yml new file mode 100644 index 00000000..a4985e0b --- /dev/null +++ b/Packages/ConfigKeyKit/.github/actions/setup-configkeykit/action.yml @@ -0,0 +1,26 @@ +name: Setup ConfigKeyKit +description: Replaces a local ConfigKeyKit path dependency with a remote branch reference + +inputs: + branch: + description: ConfigKeyKit branch to use (leave empty to keep the local path dependency) + +runs: + using: composite + steps: + - name: Update Package.swift (Unix) + if: inputs.branch != '' && runner.os != 'Windows' + shell: bash + run: | + if [ "$RUNNER_OS" = "macOS" ]; then + sed -i '' 's|\.package(name: "ConfigKeyKit", path: "[^"]*")|.package(url: "https://github.com/brightdigit/ConfigKeyKit.git", branch: "'"${{ inputs.branch }}"'")|g' Package.swift + else + sed -i 's|\.package(name: "ConfigKeyKit", path: "[^"]*")|.package(url: "https://github.com/brightdigit/ConfigKeyKit.git", branch: "'"${{ inputs.branch }}"'")|g' Package.swift + fi + rm -f Package.resolved + - name: Update Package.swift (Windows) + if: inputs.branch != '' && runner.os == 'Windows' + shell: pwsh + run: | + (Get-Content Package.swift) -replace '\.package\(name: "ConfigKeyKit", path: "[^"]*"\)', ".package(url: `"https://github.com/brightdigit/ConfigKeyKit.git`", branch: `"${{ inputs.branch }}`")" | Set-Content Package.swift + Remove-Item -Path Package.resolved -Force -ErrorAction SilentlyContinue diff --git a/Packages/ConfigKeyKit/.github/actions/setup-tools/action.yml b/Packages/ConfigKeyKit/.github/actions/setup-tools/action.yml new file mode 100644 index 00000000..069f32e9 --- /dev/null +++ b/Packages/ConfigKeyKit/.github/actions/setup-tools/action.yml @@ -0,0 +1,29 @@ +name: Setup mise tools +description: >- + Restore (or build + save) the mise tool cache and put the binaries on PATH. + Implemented as a composite action so the cache scope is the caller job's + scope — reusable workflows scope caches separately, which silently breaks + hand-off between a setup job and a consumer lint job. + +runs: + using: composite + steps: + - name: Cache mise tools + id: mise-cache + uses: actions/cache@v4 + with: + path: ~/.local/share/mise/installs + key: mise-v2-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('mise.toml') }} + restore-keys: | + mise-v2-${{ runner.os }}-${{ runner.arch }}- + - name: Install mise tools (cache miss) + if: steps.mise-cache.outputs.cache-hit != 'true' + uses: jdx/mise-action@v4 + with: + cache: false + - name: Configure PATH for cached mise tools + if: steps.mise-cache.outputs.cache-hit == 'true' + uses: jdx/mise-action@v4 + with: + install: false + cache: false diff --git a/Packages/ConfigKeyKit/.github/workflows/ConfigKeyKit.yml b/Packages/ConfigKeyKit/.github/workflows/ConfigKeyKit.yml new file mode 100644 index 00000000..2bcb8a19 --- /dev/null +++ b/Packages/ConfigKeyKit/.github/workflows/ConfigKeyKit.yml @@ -0,0 +1,288 @@ +name: ConfigKeyKit +on: + push: + branches: + - main + paths-ignore: + - '**.md' + - 'docs/**' + - 'LICENSE' + - '.github/ISSUE_TEMPLATE/**' + pull_request: + paths-ignore: + - '**.md' + - 'docs/**' + - 'LICENSE' + - '.github/ISSUE_TEMPLATE/**' + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.ref }} + cancel-in-progress: true + +env: + PACKAGE_NAME: ConfigKeyKit + +jobs: + configure: + name: Configure Matrix + runs-on: ubuntu-latest + outputs: + full-matrix: ${{ steps.check.outputs.full }} + ubuntu-os: ${{ steps.matrix.outputs.ubuntu-os }} + ubuntu-swift: ${{ steps.matrix.outputs.ubuntu-swift }} + ubuntu-type: ${{ steps.matrix.outputs.ubuntu-type }} + steps: + - id: check + name: Determine matrix scope + run: | + FULL=false + REF="${{ github.ref }}" + EVENT="${{ github.event_name }}" + BASE_REF="${{ github.base_ref }}" + + if [[ "$REF" == "refs/heads/main" ]]; then + FULL=true + elif [[ "$REF" =~ ^refs/heads/v?[0-9]+\.[0-9]+\.[0-9]+ ]]; then + FULL=true + elif [[ "$EVENT" == "pull_request" ]]; then + if [[ "$BASE_REF" == "main" || "$BASE_REF" =~ ^v?[0-9]+\.[0-9]+\.[0-9]+ ]]; then + FULL=true + fi + fi + + echo "full=$FULL" >> "$GITHUB_OUTPUT" + echo "Full matrix: $FULL (ref=$REF, event=$EVENT, base_ref=$BASE_REF)" + + - id: matrix + name: Build matrix values + run: | + if [[ "${{ steps.check.outputs.full }}" == "true" ]]; then + echo 'ubuntu-os=["noble","jammy"]' >> "$GITHUB_OUTPUT" + echo 'ubuntu-swift=[{"version":"6.2"},{"version":"6.3"}]' >> "$GITHUB_OUTPUT" + echo 'ubuntu-type=["","wasm","wasm-embedded"]' >> "$GITHUB_OUTPUT" + else + echo 'ubuntu-os=["noble"]' >> "$GITHUB_OUTPUT" + echo 'ubuntu-swift=[{"version":"6.3"}]' >> "$GITHUB_OUTPUT" + echo 'ubuntu-type=[""]' >> "$GITHUB_OUTPUT" + fi + + build-ubuntu: + name: Build on Ubuntu + needs: configure + runs-on: ubuntu-latest + container: swift:${{ matrix.swift.version }}-${{ matrix.os }} + if: ${{ !contains(github.event.head_commit.message, 'ci skip') }} + strategy: + fail-fast: false + matrix: + os: ${{ fromJSON(needs.configure.outputs.ubuntu-os) }} + swift: ${{ fromJSON(needs.configure.outputs.ubuntu-swift) }} + type: ${{ fromJSON(needs.configure.outputs.ubuntu-type) }} + steps: + - uses: actions/checkout@v6 + - uses: brightdigit/swift-build@v1 + id: build + with: + type: ${{ matrix.type }} + wasmtime-version: "40.0.2" + wasm-swift-flags: >- + -Xcc -D_WASI_EMULATED_SIGNAL + -Xcc -D_WASI_EMULATED_MMAN + -Xlinker -lwasi-emulated-signal + -Xlinker -lwasi-emulated-mman + - name: Install curl (required by Codecov uploader) + if: steps.build.outputs.contains-code-coverage == 'true' + run: | + if command -v apt-get >/dev/null 2>&1; then + apt-get update && apt-get install -y --no-install-recommends curl ca-certificates + fi + - name: Install coverage.py (silences codecov-cli probe warning) + if: steps.build.outputs.contains-code-coverage == 'true' + run: pip3 install --quiet --user coverage 2>/dev/null || true + - uses: sersoft-gmbh/swift-coverage-action@v5 + if: steps.build.outputs.contains-code-coverage == 'true' + id: coverage-files + with: + fail-on-empty-output: true + - name: Upload coverage to Codecov + if: steps.build.outputs.contains-code-coverage == 'true' + uses: codecov/codecov-action@v6 + with: + fail_ci_if_error: true + flags: swift-${{ matrix.swift.version }}-${{ matrix.os }} + verbose: true + token: ${{ secrets.CODECOV_TOKEN }} + files: ${{ join(fromJSON(steps.coverage-files.outputs.files), ',') }} + + build-windows: + name: Build on Windows + needs: configure + runs-on: ${{ matrix.runs-on }} + if: ${{ needs.configure.outputs.full-matrix == 'true' && !contains(github.event.head_commit.message, 'ci skip') }} + strategy: + fail-fast: false + matrix: + runs-on: [windows-2022, windows-2025] + swift: + - version: swift-6.2-release + build: 6.2-RELEASE + - version: swift-6.3-release + build: 6.3-RELEASE + steps: + - uses: actions/checkout@v6 + - uses: brightdigit/swift-build@v1 + id: build + with: + windows-swift-version: ${{ matrix.swift.version }} + windows-swift-build: ${{ matrix.swift.build }} + - name: Upload coverage to Codecov + if: steps.build.outputs.contains-code-coverage == 'true' + uses: codecov/codecov-action@v6 + with: + fail_ci_if_error: true + flags: swift-${{ matrix.swift.version }},windows + verbose: true + token: ${{ secrets.CODECOV_TOKEN }} + os: windows + swift_project: ConfigKeyKit + + build-android: + name: Build on Android + needs: configure + runs-on: ubuntu-latest + if: ${{ needs.configure.outputs.full-matrix == 'true' && !contains(github.event.head_commit.message, 'ci skip') }} + strategy: + fail-fast: false + matrix: + swift: + - version: "6.2" + - version: "6.3" + android-api-level: [33, 34] + steps: + - uses: actions/checkout@v6 + - name: Free disk space + uses: jlumbroso/free-disk-space@v1.3.1 + with: + tool-cache: false + android: false + dotnet: true + haskell: true + large-packages: true + docker-images: true + swap-storage: true + - uses: brightdigit/swift-build@v1 + with: + type: android + android-swift-version: ${{ matrix.swift.version }} + android-api-level: ${{ matrix.android-api-level }} + android-run-tests: true + + build-macos: + name: Build on macOS + runs-on: macos-26 + if: ${{ !contains(github.event.head_commit.message, 'ci skip') }} + strategy: + fail-fast: false + matrix: + include: + - xcode: "/Applications/Xcode_26.4.app" + - type: ios + xcode: "/Applications/Xcode_26.4.app" + deviceName: "iPhone 17 Pro" + osVersion: "26.4.1" + download-platform: true + steps: + - uses: actions/checkout@v6 + - name: Build and Test + id: build + uses: brightdigit/swift-build@v1 + with: + scheme: ${{ env.PACKAGE_NAME }} + type: ${{ matrix.type }} + xcode: ${{ matrix.xcode }} + deviceName: ${{ matrix.deviceName }} + osVersion: ${{ matrix.osVersion }} + download-platform: ${{ matrix.download-platform }} + - name: Install coverage.py (silences codecov-cli probe warning) + if: steps.build.outputs.contains-code-coverage == 'true' + run: pip3 install --quiet --user coverage 2>/dev/null || true + - name: Process Coverage + if: steps.build.outputs.contains-code-coverage == 'true' + uses: sersoft-gmbh/swift-coverage-action@v5 + - name: Upload Coverage + if: steps.build.outputs.contains-code-coverage == 'true' + uses: codecov/codecov-action@v6 + with: + token: ${{ secrets.CODECOV_TOKEN }} + flags: ${{ matrix.type && format('{0}{1}', matrix.type, matrix.osVersion) || 'spm' }} + + build-macos-platforms: + name: Build on macOS (Platforms) + needs: configure + runs-on: ${{ matrix.runs-on }} + if: ${{ needs.configure.outputs.full-matrix == 'true' && !contains(github.event.head_commit.message, 'ci skip') }} + strategy: + fail-fast: false + matrix: + include: + - type: macos + runs-on: macos-26 + xcode: "/Applications/Xcode_26.4.app" + - type: watchos + runs-on: macos-26 + xcode: "/Applications/Xcode_26.4.app" + deviceName: "Apple Watch Ultra 3 (49mm)" + osVersion: "26.4" + download-platform: true + - type: tvos + runs-on: macos-26 + xcode: "/Applications/Xcode_26.4.app" + deviceName: "Apple TV" + osVersion: "26.4" + download-platform: true + - type: visionos + runs-on: macos-26 + xcode: "/Applications/Xcode_26.4.app" + deviceName: "Apple Vision Pro" + osVersion: "26.4.1" + download-platform: true + steps: + - uses: actions/checkout@v6 + - name: Build and Test + id: build + uses: brightdigit/swift-build@v1 + with: + scheme: ${{ env.PACKAGE_NAME }} + type: ${{ matrix.type }} + xcode: ${{ matrix.xcode }} + deviceName: ${{ matrix.deviceName }} + osVersion: ${{ matrix.osVersion }} + download-platform: ${{ matrix.download-platform }} + - name: Install coverage.py (silences codecov-cli probe warning) + if: steps.build.outputs.contains-code-coverage == 'true' + run: pip3 install --quiet --user coverage 2>/dev/null || true + - name: Process Coverage + if: steps.build.outputs.contains-code-coverage == 'true' + uses: sersoft-gmbh/swift-coverage-action@v5 + - name: Upload Coverage + if: steps.build.outputs.contains-code-coverage == 'true' + uses: codecov/codecov-action@v6 + with: + token: ${{ secrets.CODECOV_TOKEN }} + flags: ${{ matrix.type && format('{0}{1}', matrix.type, matrix.osVersion) || 'spm' }} + + lint: + name: Linting + runs-on: ubuntu-latest + if: ${{ !cancelled() && !failure() && !contains(github.event.head_commit.message, 'ci skip') }} + needs: [build-ubuntu, build-macos, build-macos-platforms, build-windows, build-android] + concurrency: + group: lint-tools-${{ github.head_ref || github.ref }} + cancel-in-progress: false + steps: + - uses: actions/checkout@v6 + - uses: ./.github/actions/setup-tools + - name: Lint + run: | + set -e + ./Scripts/lint.sh diff --git a/Packages/ConfigKeyKit/.github/workflows/check-unsafe-flags.yml b/Packages/ConfigKeyKit/.github/workflows/check-unsafe-flags.yml new file mode 100644 index 00000000..348f4430 --- /dev/null +++ b/Packages/ConfigKeyKit/.github/workflows/check-unsafe-flags.yml @@ -0,0 +1,39 @@ +name: Check for unsafeFlags + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +jobs: + dump-package-check: + name: Dump Swift package (authoritative) and scan JSON + runs-on: ubuntu-latest + container: + image: swift:latest + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Install jq + run: | + apt-get update && apt-get install -y jq + + - name: Dump package JSON and check for unsafeFlags + shell: bash + run: | + set -euo pipefail + # Compute unsafeFlags array directly from the dump (don't store the full dump variable) + unsafe_flags=$(swift package dump-package | jq -c '[.. | objects | .unsafeFlags? // empty]') + # Check array length to decide failure + if [ "$(echo "$unsafe_flags" | jq 'length')" -gt 0 ]; then + echo "ERROR: unsafeFlags found in resolved package JSON:" + echo "$unsafe_flags" | jq '.' || true + echo "--- resolved package dump (first 200 lines) ---" + # Print a sample of the authoritative dump (re-run dump-package for the sample) + swift package dump-package | sed -n '1,200p' || true + exit 1 + else + echo "No unsafeFlags in resolved package JSON." + fi diff --git a/Packages/ConfigKeyKit/.github/workflows/claude-code-review.yml b/Packages/ConfigKeyKit/.github/workflows/claude-code-review.yml new file mode 100644 index 00000000..9adfd522 --- /dev/null +++ b/Packages/ConfigKeyKit/.github/workflows/claude-code-review.yml @@ -0,0 +1,54 @@ +name: Claude Code Review + +on: + pull_request: + types: [opened, synchronize, ready_for_review, reopened] + # Optional: Only run on specific file changes + # paths: + # - "src/**/*.ts" + # - "src/**/*.tsx" + # - "src/**/*.js" + # - "src/**/*.jsx" + +jobs: + claude-review: + # Optional: Filter by PR author + # if: | + # github.event.pull_request.user.login == 'external-contributor' || + # github.event.pull_request.user.login == 'new-developer' || + # github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR' + + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: read + issues: read + id-token: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - name: Run Claude Code Review + id: claude-review + uses: anthropics/claude-code-action@v1 + with: + claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} + allowed_bots: 'codefactor-io[bot]' + prompt: | + Please review this pull request and provide feedback on: + - Code quality and best practices + - Potential bugs or issues + - Performance considerations + - Security concerns + - Test coverage + + Use the repository's CLAUDE.md for guidance on style and conventions. Be constructive and helpful in your feedback. + + Use `gh pr comment` with your Bash tool to leave your review as a comment on the PR. + + # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md + # or https://docs.anthropic.com/en/docs/claude-code/sdk#command-line for available options + claude_args: '--model sonnet --allowed-tools "Bash(gh issue view:*),Bash(gh search:*),Bash(gh issue list:*),Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*),Bash(gh pr list:*)"' \ No newline at end of file diff --git a/Packages/ConfigKeyKit/.github/workflows/claude.yml b/Packages/ConfigKeyKit/.github/workflows/claude.yml new file mode 100644 index 00000000..d300267f --- /dev/null +++ b/Packages/ConfigKeyKit/.github/workflows/claude.yml @@ -0,0 +1,50 @@ +name: Claude Code + +on: + issue_comment: + types: [created] + pull_request_review_comment: + types: [created] + issues: + types: [opened, assigned] + pull_request_review: + types: [submitted] + +jobs: + claude: + if: | + (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) || + (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) || + (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) || + (github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude'))) + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: read + issues: read + id-token: write + actions: read # Required for Claude to read CI results on PRs + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - name: Run Claude Code + id: claude + uses: anthropics/claude-code-action@v1 + with: + claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} + + # This is an optional setting that allows Claude to read CI results on PRs + additional_permissions: | + actions: read + + # Optional: Give a custom prompt to Claude. If this is not specified, Claude will perform the instructions specified in the comment that tagged it. + # prompt: 'Update the pull request description to include a summary of changes.' + + # Optional: Add claude_args to customize behavior and configuration + # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md + # or https://code.claude.com/docs/en/cli-reference for available options + # claude_args: '--allowed-tools Bash(gh pr:*)' + diff --git a/Packages/ConfigKeyKit/.github/workflows/cleanup-caches.yml b/Packages/ConfigKeyKit/.github/workflows/cleanup-caches.yml new file mode 100644 index 00000000..f0124e2c --- /dev/null +++ b/Packages/ConfigKeyKit/.github/workflows/cleanup-caches.yml @@ -0,0 +1,29 @@ +name: Cleanup Branch Caches +on: + delete: + +jobs: + cleanup: + runs-on: ubuntu-latest + permissions: + actions: write + steps: + - name: Cleanup caches for deleted branch + uses: actions/github-script@v9 + with: + script: | + const ref = `refs/heads/${context.payload.ref}`; + const caches = await github.rest.actions.getActionsCacheList({ + owner: context.repo.owner, + repo: context.repo.repo, + ref: ref, + }); + for (const cache of caches.data.actions_caches) { + console.log(`Deleting cache: ${cache.key}`); + await github.rest.actions.deleteActionsCacheById({ + owner: context.repo.owner, + repo: context.repo.repo, + cache_id: cache.id, + }); + } + console.log(`Deleted ${caches.data.actions_caches.length} cache(s) for ${ref}`); diff --git a/Packages/ConfigKeyKit/.github/workflows/codeql.yml b/Packages/ConfigKeyKit/.github/workflows/codeql.yml new file mode 100644 index 00000000..2b3b13fe --- /dev/null +++ b/Packages/ConfigKeyKit/.github/workflows/codeql.yml @@ -0,0 +1,82 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL" + +on: + push: + branches-ignore: + - '*WIP' + pull_request: + # The branches below must be a subset of the branches above + branches: [ "main" ] + schedule: + - cron: '20 11 * * 3' + +jobs: + analyze: + name: Analyze + # CodeQL Swift analysis requires macOS runners — Linux is not supported + # ("Swift analysis is only supported on macOS runner images"). Other languages + # can run on Linux, hence the conditional. + runs-on: ${{ (matrix.language == 'swift' && 'macos-26') || 'ubuntu-latest' }} + timeout-minutes: ${{ (matrix.language == 'swift' && 120) || 360 }} + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: [ 'swift' ] + # CodeQL supports [ 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift' ] + # Use only 'java-kotlin' to analyze code written in Java, Kotlin or both + # Use only 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both + # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support + + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Setup Xcode + if: matrix.language == 'swift' + run: sudo xcode-select -s /Applications/Xcode_26.4.app/Contents/Developer + + - name: Verify Swift Version + if: matrix.language == 'swift' + run: | + swift --version + swift package --version + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v4 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + + # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs + # queries: security-extended,security-and-quality + + + # Autobuild attempts to build any compiled languages (C/C++, C#, Go, Java, or Swift). + # If this step fails, then you should remove it and run the build manually (see below) + - run: | + echo "Run, Build Application using script" + swift build + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v4 + with: + category: "/language:${{matrix.language}}" diff --git a/Packages/ConfigKeyKit/.github/workflows/swift-source-compat.yml b/Packages/ConfigKeyKit/.github/workflows/swift-source-compat.yml new file mode 100644 index 00000000..982157d1 --- /dev/null +++ b/Packages/ConfigKeyKit/.github/workflows/swift-source-compat.yml @@ -0,0 +1,29 @@ +name: Swift Source Compatibility + +on: + push: + branches: [main] + pull_request: + workflow_dispatch: + +jobs: + swift-source-compat-suite: + name: Test Swift ${{ matrix.container }} For Source Compatibility Suite + runs-on: ubuntu-latest + if: ${{ !contains(github.event.head_commit.message, 'ci skip') }} + + strategy: + fail-fast: false + matrix: + container: + - swift:6.2 + - swift:6.3 + + container: ${{ matrix.container }} + + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Test Swift 6.x For Source Compatibility + run: swift build --disable-sandbox --verbose --configuration release diff --git a/Packages/ConfigKeyKit/.gitignore b/Packages/ConfigKeyKit/.gitignore new file mode 100644 index 00000000..20909d84 --- /dev/null +++ b/Packages/ConfigKeyKit/.gitignore @@ -0,0 +1,84 @@ +# macOS +.DS_Store + +# Xcode +# +# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore + +## User settings +xcuserdata/ + +## Obj-C/Swift specific +*.hmap + +## App packaging +*.ipa +*.dSYM.zip +*.dSYM + +## Playgrounds +timeline.xctimeline +playground.xcworkspace + +# Swift Package Manager +# +# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. +# Packages/ +# Package.pins +# *.xcodeproj +# +# Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata +# hence it is not needed unless you have added a package configuration file to your project +Package.resolved +.swiftpm/ +.build/ +DerivedData/ +.index-build/ + +# Generated Xcode projects/workspaces +*.xcodeproj +*.xcworkspace + +# CocoaPods +# +# We recommend against adding the Pods directory to your .gitignore. However +# you should judge for yourself, the pros and cons are mentioned at: +# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control +# +# Pods/ +# +# Add this line if you want to avoid checking in source code from the Xcode workspace +# *.xcworkspace + +# Carthage +# +# Add this line if you want to avoid checking in source code from Carthage dependencies. +# Carthage/Checkouts + +Carthage/Build/ + +# fastlane +# +# It is recommended to not store the screenshots in the git repo. +# Instead, use fastlane to re-generate the screenshots whenever they are needed. +# For more information about the recommended setup visit: +# https://docs.fastlane.tools/best-practices/source-control/#source-control + +fastlane/report.xml +fastlane/Preview.html +fastlane/screenshots/**/*.png +fastlane/test_output + +# IDE +.vscode/ +.idea/ + +# mise / mint local installs +.mint/ + +# Editor scratch +*.sw? + +# Claude +.claude/settings.local.json +.claude/scheduled_tasks.lock diff --git a/Packages/ConfigKeyKit/.gitrepo b/Packages/ConfigKeyKit/.gitrepo new file mode 100644 index 00000000..0d4e3142 --- /dev/null +++ b/Packages/ConfigKeyKit/.gitrepo @@ -0,0 +1,12 @@ +; DO NOT EDIT (unless you know what you are doing) +; +; This subdirectory is a git "subrepo", and this file is maintained by the +; git-subrepo command. See https://github.com/ingydotnet/git-subrepo#readme +; +[subrepo] + remote = git@github.com:brightdigit/ConfigKeyKit.git + branch = main + commit = a9a8bc8be5b33d4aa732a9d0d06a05e8281b4855 + parent = 5d1a87aeaffbdfc883b2d13467503dda592d1ec0 + method = merge + cmdver = 0.4.9 diff --git a/Packages/ConfigKeyKit/.periphery.yml b/Packages/ConfigKeyKit/.periphery.yml new file mode 100644 index 00000000..963c035a --- /dev/null +++ b/Packages/ConfigKeyKit/.periphery.yml @@ -0,0 +1,3 @@ +retain_public: true +retain_unused_protocol_func_params: true +retain_assign_only_properties: true diff --git a/Packages/ConfigKeyKit/.spi.yml b/Packages/ConfigKeyKit/.spi.yml new file mode 100644 index 00000000..bd205001 --- /dev/null +++ b/Packages/ConfigKeyKit/.spi.yml @@ -0,0 +1,5 @@ +version: 1 +builder: + configs: + - documentation_targets: [ConfigKeyKit] + swift_version: 6.2 diff --git a/Packages/ConfigKeyKit/.swift-format b/Packages/ConfigKeyKit/.swift-format new file mode 100644 index 00000000..257f5557 --- /dev/null +++ b/Packages/ConfigKeyKit/.swift-format @@ -0,0 +1,70 @@ +{ + "fileScopedDeclarationPrivacy" : { + "accessLevel" : "fileprivate" + }, + "indentation" : { + "spaces" : 2 + }, + "indentConditionalCompilationBlocks" : true, + "indentSwitchCaseLabels" : false, + "lineBreakAroundMultilineExpressionChainComponents" : false, + "lineBreakBeforeControlFlowKeywords" : false, + "lineBreakBeforeEachArgument" : false, + "lineBreakBeforeEachGenericRequirement" : false, + "lineLength" : 100, + "maximumBlankLines" : 1, + "multiElementCollectionTrailingCommas" : true, + "noAssignmentInExpressions" : { + "allowedFunctions" : [ + "XCTAssertNoThrow" + ] + }, + "prioritizeKeepingFunctionOutputTogether" : false, + "respectsExistingLineBreaks" : true, + "rules" : { + "AllPublicDeclarationsHaveDocumentation" : true, + "AlwaysUseLiteralForEmptyCollectionInit" : false, + "AlwaysUseLowerCamelCase" : true, + "AmbiguousTrailingClosureOverload" : true, + "BeginDocumentationCommentWithOneLineSummary" : false, + "DoNotUseSemicolons" : true, + "DontRepeatTypeInStaticProperties" : true, + "FileScopedDeclarationPrivacy" : false, + "FullyIndirectEnum" : true, + "GroupNumericLiterals" : true, + "IdentifiersMustBeASCII" : true, + "NeverForceUnwrap" : true, + "NeverUseForceTry" : true, + "NeverUseImplicitlyUnwrappedOptionals" : true, + "NoAccessLevelOnExtensionDeclaration" : true, + "NoAssignmentInExpressions" : true, + "NoBlockComments" : true, + "NoCasesWithOnlyFallthrough" : true, + "NoEmptyTrailingClosureParentheses" : true, + "NoLabelsInCasePatterns" : true, + "NoLeadingUnderscores" : true, + "NoParensAroundConditions" : true, + "NoPlaygroundLiterals" : true, + "NoVoidReturnOnFunctionSignature" : true, + "OmitExplicitReturns" : false, + "OneCasePerLine" : true, + "OneVariableDeclarationPerLine" : true, + "OnlyOneTrailingClosureArgument" : true, + "OrderedImports" : true, + "ReplaceForEachWithForLoop" : true, + "ReturnVoidInsteadOfEmptyTuple" : true, + "TypeNamesShouldBeCapitalized" : true, + "UseEarlyExits" : false, + "UseExplicitNilCheckInConditions" : true, + "UseLetInEveryBoundCaseVariable" : true, + "UseShorthandTypeNames" : true, + "UseSingleLinePropertyGetter" : true, + "UseSynthesizedInitializer" : true, + "UseTripleSlashForDocumentationComments" : true, + "UseWhereClausesInForLoops" : true, + "ValidateDocumentationComments" : true + }, + "spacesAroundRangeFormationOperators" : false, + "tabWidth" : 2, + "version" : 1 +} diff --git a/Packages/ConfigKeyKit/.swift-version b/Packages/ConfigKeyKit/.swift-version new file mode 100644 index 00000000..f9da12e1 --- /dev/null +++ b/Packages/ConfigKeyKit/.swift-version @@ -0,0 +1 @@ +6.3.2 \ No newline at end of file diff --git a/Packages/ConfigKeyKit/.swiftlint.yml b/Packages/ConfigKeyKit/.swiftlint.yml new file mode 100644 index 00000000..08f1c1fc --- /dev/null +++ b/Packages/ConfigKeyKit/.swiftlint.yml @@ -0,0 +1,141 @@ +opt_in_rules: + - array_init + - closure_body_length + - closure_end_indentation + - closure_spacing + - collection_alignment + - conditional_returns_on_newline + - contains_over_filter_count + - contains_over_filter_is_empty + - contains_over_first_not_nil + - contains_over_range_nil_comparison + - convenience_type + - discouraged_object_literal + - discouraged_optional_boolean + - empty_collection_literal + - empty_count + - empty_string + - empty_xctest_method + - enum_case_associated_values_count + - expiring_todo + - explicit_acl + - explicit_init + - explicit_top_level_acl + - fatal_error_message + - file_name + - file_name_no_space + - file_types_order + - first_where + - flatmap_over_map_reduce + - force_unwrapping + - ibinspectable_in_extension + - identical_operands + - implicit_return + - implicitly_unwrapped_optional + - indentation_width + - joined_default_parameter + - last_where + - legacy_multiple + - legacy_random + - literal_expression_end_indentation + - lower_acl_than_parent + - missing_docs + - modifier_order + - multiline_arguments + - multiline_arguments_brackets + - multiline_function_chains + - multiline_literal_brackets + - multiline_parameters + - nimble_operator + - nslocalizedstring_key + - nslocalizedstring_require_bundle + - number_separator + - object_literal + - one_declaration_per_file + - operator_usage_whitespace + - optional_enum_case_matching + - overridden_super_call + - override_in_extension + - pattern_matching_keywords + - prefer_self_type_over_type_of_self + - prefer_zero_over_explicit_init + - private_action + - private_outlet + - prohibited_interface_builder + - prohibited_super_call + - quick_discouraged_call + - quick_discouraged_focused_test + - quick_discouraged_pending_test + - reduce_into + - redundant_nil_coalescing + - redundant_type_annotation + - required_enum_case + - single_test_class + - sorted_first_last + - sorted_imports + - static_operator + - strong_iboutlet + - toggle_bool + - type_contents_order + - unavailable_function + - unneeded_parentheses_in_closure_argument + - unowned_variable_capture + - untyped_error_in_catch + - vertical_parameter_alignment_on_call + - vertical_whitespace_closing_braces + - vertical_whitespace_opening_braces + - xct_specific_matcher + - yoda_condition +analyzer_rules: + - unused_import + - unused_declaration +cyclomatic_complexity: + - 6 + - 12 +file_length: + warning: 225 + error: 300 +function_body_length: + - 50 + - 76 +function_parameter_count: 8 +line_length: + - 108 + - 200 +closure_body_length: + - 50 + - 60 +type_name: + min_length: 3 + max_length: + warning: 50 + error: 60 +identifier_name: + excluded: + - id + - no +excluded: + - DerivedData + - .build + - Package.swift +indentation_width: + indentation_width: 2 +file_name: + severity: error +fatal_error_message: + severity: error +disabled_rules: + - nesting + - implicit_getter + - switch_case_alignment + - closure_parameter_position + - trailing_comma + - opening_brace + - optional_data_string_conversion + - pattern_matching_keywords +custom_rules: + no_unchecked_sendable: + name: "No Unchecked Sendable" + regex: '@unchecked\s+Sendable' + message: "Use proper Sendable conformance instead of @unchecked Sendable to maintain strict concurrency safety" + severity: error diff --git a/Packages/ConfigKeyKit/LICENSE b/Packages/ConfigKeyKit/LICENSE new file mode 100644 index 00000000..5bf4bad4 --- /dev/null +++ b/Packages/ConfigKeyKit/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 BrightDigit + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Packages/ConfigKeyKit/Makefile b/Packages/ConfigKeyKit/Makefile new file mode 100644 index 00000000..96f245d6 --- /dev/null +++ b/Packages/ConfigKeyKit/Makefile @@ -0,0 +1,22 @@ +.PHONY: help build test lint clean + +help: + @echo "Available targets:" + @echo " build - Build the package" + @echo " test - Run the test suite" + @echo " lint - Run swift-format + swiftlint + periphery via Scripts/lint.sh" + @echo " clean - Clean build artifacts" + @echo " help - Show this help message" + +build: + swift build + +test: + swift test + +lint: + @./Scripts/lint.sh + +clean: + swift package clean + rm -rf .build diff --git a/Packages/ConfigKeyKit/Package.swift b/Packages/ConfigKeyKit/Package.swift new file mode 100644 index 00000000..315080a3 --- /dev/null +++ b/Packages/ConfigKeyKit/Package.swift @@ -0,0 +1,47 @@ +// swift-tools-version: 6.2 + +// swiftlint:disable explicit_acl explicit_top_level_acl + +import PackageDescription + +// MARK: - Swift Settings Configuration + +let swiftSettings: [SwiftSetting] = [ + // Swift 6.2 Upcoming Features (not yet enabled by default) + // SE-0335: Introduce existential `any` + .enableUpcomingFeature("ExistentialAny"), + // SE-0409: Access-level modifiers on import declarations + .enableUpcomingFeature("InternalImportsByDefault"), + // SE-0444: Member import visibility (Swift 6.1+) + .enableUpcomingFeature("MemberImportVisibility"), + // SE-0413: Typed throws + .enableUpcomingFeature("FullTypedThrows"), +] + +let package = Package( + name: "ConfigKeyKit", + platforms: [ + .macOS(.v15), + .iOS(.v18), + .tvOS(.v18), + .watchOS(.v11), + .visionOS(.v2), + ], + products: [ + .library(name: "ConfigKeyKit", targets: ["ConfigKeyKit"]), + ], + targets: [ + .target( + name: "ConfigKeyKit", + dependencies: [], + swiftSettings: swiftSettings + ), + .testTarget( + name: "ConfigKeyKitTests", + dependencies: ["ConfigKeyKit"], + swiftSettings: swiftSettings + ), + ] +) + +// swiftlint:enable explicit_acl explicit_top_level_acl diff --git a/Packages/ConfigKeyKit/README.md b/Packages/ConfigKeyKit/README.md new file mode 100644 index 00000000..a7a413e6 --- /dev/null +++ b/Packages/ConfigKeyKit/README.md @@ -0,0 +1,106 @@ +# ConfigKeyKit + +A tiny, dependency-free Swift package for defining configuration keys that +resolve consistently across multiple sources (command-line arguments, +environment variables, …). + +ConfigKeyKit pairs naturally with [`apple/swift-configuration`][swift-config] +but does **not** depend on it — the package is intentionally Foundation-only +so it can be adopted by any Swift app or library without pulling in a +configuration framework. + +## What's inside + +A single `ConfigKeyKit` product bundles: + +- **Key types** — `ConfigKey`, `OptionalConfigKey`, `ConfigurationKey`, `NamingStyle`, `StandardNamingStyle`, `ConfigKeySource`. +- **CLI scaffolding** — `Command`, `CommandRegistry`, `CommandLineParser`, `ConfigurationParseable`. A lightweight command-dispatch pattern you can ignore if you only need keys. + +## Usage + +```swift +import ConfigKeyKit + +let containerID = ConfigKey( + "cloudkit.container_id", + envPrefix: "MYAPP", + default: "iCloud.com.example.MyApp" +) + +containerID.key(for: .commandLine) // "cloudkit.container_id" +containerID.key(for: .environment) // "MYAPP_CLOUDKIT_CONTAINER_ID" +containerID.defaultValue // "iCloud.com.example.MyApp" +``` + +You then feed those resolved key strings into whatever provider stack you +prefer (Swift Configuration, environment lookup, manual argument parsing, …). + +## Pairing with `swift-configuration` + +`ConfigKey` resolves a single base key into per-source strings, which slots +neatly into [`swift-configuration`][swift-config]'s provider stack. Each +provider sees the key name it expects (dot-separated for CLI, screaming snake +case for ENV), while your call site stays declarative: + +```swift +import Configuration +import ConfigKeyKit + +let containerID = ConfigKey( + "cloudkit.container_id", + envPrefix: "MYAPP", + default: "iCloud.com.example.MyApp" +) + +let config = ConfigReader(providers: [ + CommandLineArgumentsProvider(), + EnvironmentVariablesProvider(), +]) + +extension ConfigReader { + func string(for key: ConfigKey) -> String where Value == String { + if let cli = key.key(for: .commandLine), + let value = self.string(forKey: cli) { + return value + } + if let env = key.key(for: .environment), + let value = self.string(forKey: env) { + return value + } + return key.defaultValue + } +} + +let resolved = config.string(for: containerID) +// Looks up "cloudkit.container_id" on the CLI, falls back to +// "MYAPP_CLOUDKIT_CONTAINER_ID" in the environment, then the default. +``` + +## Used by + +- [MistDemo][mistdemo] — the demo app inside [MistKit][mistkit]. +- [BushelCloud][bushelcloud]. + +## Adding to your `Package.swift` + +```swift +.package(url: "https://github.com/brightdigit/ConfigKeyKit.git", from: "1.0.0-beta.1"), +``` + +```swift +.target( + name: "MyApp", + dependencies: [ + .product(name: "ConfigKeyKit", package: "ConfigKeyKit"), + ] +), +``` + +## License + +MIT — see [LICENSE](LICENSE). + +[swift-config]: https://github.com/apple/swift-configuration +[mistkit]: https://github.com/brightdigit/MistKit +[mistdemo]: https://github.com/brightdigit/MistKit/tree/main/Examples/MistDemo +[bushelcloud]: https://github.com/brightdigit/BushelCloud diff --git a/Packages/ConfigKeyKit/Scripts/header.sh b/Packages/ConfigKeyKit/Scripts/header.sh new file mode 100755 index 00000000..809f88ac --- /dev/null +++ b/Packages/ConfigKeyKit/Scripts/header.sh @@ -0,0 +1,113 @@ +#!/bin/bash + +# Function to print usage +usage() { + echo "Usage: $0 -d directory -c creator -o company -p package [-y year]" + echo " -d directory Directory to read from (including subdirectories)" + echo " -c creator Name of the creator" + echo " -o company Name of the company with the copyright" + echo " -p package Package or library name" + echo " -y year Copyright year (optional, defaults to current year)" + exit 1 +} + +# Get the current year if not provided +current_year=$(date +"%Y") + +# Default values +year="$current_year" + +# Parse arguments +while getopts ":d:c:o:p:y:" opt; do + case $opt in + d) directory="$OPTARG" ;; + c) creator="$OPTARG" ;; + o) company="$OPTARG" ;; + p) package="$OPTARG" ;; + y) year="$OPTARG" ;; + *) usage ;; + esac +done + +# Check for mandatory arguments +if [ -z "$directory" ] || [ -z "$creator" ] || [ -z "$company" ] || [ -z "$package" ]; then + usage +fi + +# Define the header template using a heredoc +read -r -d '' header_template <<'EOF' +// +// %s +// %s +// +// Created by %s. +// Copyright © %s %s. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// +EOF + +# Loop through each Swift file in the specified directory and subdirectories +find "$directory" -type f -name "*.swift" | while read -r file; do + # Skip files carrying `// swift-format-ignore-file` anywhere in the leading + # comment block. This is the opt-out used by generated files (e.g. + # swift-openapi-generator emits it via `additionalFileComments`) and lets + # them sit anywhere in the tree without needing a path-based exclusion. + if awk ' + /^\/\/[[:space:]]*swift-format-ignore-file[[:space:]]*$/ { found = 1; exit } + /^[[:space:]]*$/ || /^\/\// { next } + { exit } + END { exit !found } + ' "$file"; then + echo "Skipping $file due to swift-format-ignore directive." + continue + fi + + # Create the header with the current filename + # Escape % characters in user-provided values to prevent format specifier injection + filename=$(basename "$file" | sed 's/%/%%/g') + package_safe=$(printf '%s' "$package" | sed 's/%/%%/g') + creator_safe=$(printf '%s' "$creator" | sed 's/%/%%/g') + year_safe=$(printf '%s' "$year" | sed 's/%/%%/g') + company_safe=$(printf '%s' "$company" | sed 's/%/%%/g') + + header=$(printf "$header_template" "$filename" "$package_safe" "$creator_safe" "$year_safe" "$company_safe") + + # Remove all consecutive lines at the beginning which start with "// ", contain only whitespace, or only "//" + awk ' + BEGIN { skip = 1 } + { + if (skip && ($0 ~ /^\/\/ / || $0 ~ /^\/\/$/ || $0 ~ /^$/)) { + next + } + skip = 0 + print + }' "$file" > temp_file + + # Add the header to the cleaned file + (echo "$header"; echo; cat temp_file) > "$file" + + # Remove the temporary file + rm temp_file +done + +echo "Headers added or files skipped appropriately across all Swift files in the directory and subdirectories." diff --git a/Packages/ConfigKeyKit/Scripts/lint.sh b/Packages/ConfigKeyKit/Scripts/lint.sh new file mode 100755 index 00000000..e2e602b6 --- /dev/null +++ b/Packages/ConfigKeyKit/Scripts/lint.sh @@ -0,0 +1,67 @@ +#!/bin/bash + +# Remove set -e to allow script to continue running + +ERRORS=0 + +run_command() { + "$@" || ERRORS=$((ERRORS + 1)) +} + +if [ "$LINT_MODE" = "INSTALL" ]; then + exit +fi + +echo "LintMode: $LINT_MODE" + +# More portable way to get script directory +if [ -z "$SRCROOT" ]; then + SCRIPT_DIR=$(dirname "$(readlink -f "$0")") + PACKAGE_DIR="${SCRIPT_DIR}/.." +else + PACKAGE_DIR="${SRCROOT}" +fi + +# Ensure mise-managed tools are on PATH outside CI (CI uses jdx/mise-action) +if command -v mise >/dev/null 2>&1 && [ -z "$CI" ]; then + eval "$(mise -C "$PACKAGE_DIR" env -s bash)" +fi + +if [ "$LINT_MODE" = "NONE" ]; then + exit +elif [ "$LINT_MODE" = "STRICT" ]; then + SWIFTFORMAT_OPTIONS="--configuration .swift-format" + SWIFTLINT_OPTIONS="--strict" +else + SWIFTFORMAT_OPTIONS="--configuration .swift-format" + SWIFTLINT_OPTIONS="" +fi + +pushd $PACKAGE_DIR + +if [ -z "$CI" ]; then + run_command swift-format format $SWIFTFORMAT_OPTIONS --recursive --parallel --in-place Sources Tests + run_command swiftlint --fix +fi + +if [ -z "$FORMAT_ONLY" ]; then + run_command swift-format lint --configuration .swift-format --recursive --parallel $SWIFTFORMAT_OPTIONS Sources Tests + run_command swiftlint lint $SWIFTLINT_OPTIONS + run_command swift build --build-tests +fi + +$PACKAGE_DIR/Scripts/header.sh -d $PACKAGE_DIR/Sources -c "Leo Dion" -o "BrightDigit" -p "ConfigKeyKit" + +if [ -z "$CI" ]; then + run_command periphery scan $PERIPHERY_OPTIONS --disable-update-check +fi + +popd + +if [ $ERRORS -gt 0 ]; then + echo "Linting completed with $ERRORS error(s)" + exit 1 +else + echo "Linting completed successfully" + exit 0 +fi diff --git a/Examples/MistDemo/Sources/ConfigKeyKit/Command.swift b/Packages/ConfigKeyKit/Sources/ConfigKeyKit/Command.swift similarity index 97% rename from Examples/MistDemo/Sources/ConfigKeyKit/Command.swift rename to Packages/ConfigKeyKit/Sources/ConfigKeyKit/Command.swift index f9baa1cc..f3743061 100644 --- a/Examples/MistDemo/Sources/ConfigKeyKit/Command.swift +++ b/Packages/ConfigKeyKit/Sources/ConfigKeyKit/Command.swift @@ -1,6 +1,6 @@ // // Command.swift -// MistDemo +// ConfigKeyKit // // Created by Leo Dion. // Copyright © 2026 BrightDigit. @@ -27,8 +27,6 @@ // OTHER DEALINGS IN THE SOFTWARE. // -internal import Foundation - /// Generic protocol for CLI commands using Swift Configuration public protocol Command: Sendable { /// Associated configuration type for this command diff --git a/Examples/MistDemo/Sources/ConfigKeyKit/CommandConfiguration.swift b/Packages/ConfigKeyKit/Sources/ConfigKeyKit/CommandConfiguration.swift similarity index 96% rename from Examples/MistDemo/Sources/ConfigKeyKit/CommandConfiguration.swift rename to Packages/ConfigKeyKit/Sources/ConfigKeyKit/CommandConfiguration.swift index c3c21924..251449e4 100644 --- a/Examples/MistDemo/Sources/ConfigKeyKit/CommandConfiguration.swift +++ b/Packages/ConfigKeyKit/Sources/ConfigKeyKit/CommandConfiguration.swift @@ -1,6 +1,6 @@ // // CommandConfiguration.swift -// MistDemo +// ConfigKeyKit // // Created by Leo Dion. // Copyright © 2026 BrightDigit. @@ -28,7 +28,7 @@ // /// Command configuration for identifying and routing commands -public struct CommandConfiguration { +public struct CommandConfiguration: Sendable { /// The name used to invoke this command on the CLI. public let commandName: String /// A short description of what the command does. diff --git a/Examples/MistDemo/Sources/ConfigKeyKit/CommandLineParser.swift b/Packages/ConfigKeyKit/Sources/ConfigKeyKit/CommandLineParser.swift similarity index 97% rename from Examples/MistDemo/Sources/ConfigKeyKit/CommandLineParser.swift rename to Packages/ConfigKeyKit/Sources/ConfigKeyKit/CommandLineParser.swift index 1e03d791..22aa09f5 100644 --- a/Examples/MistDemo/Sources/ConfigKeyKit/CommandLineParser.swift +++ b/Packages/ConfigKeyKit/Sources/ConfigKeyKit/CommandLineParser.swift @@ -1,6 +1,6 @@ // // CommandLineParser.swift -// MistDemo +// ConfigKeyKit // // Created by Leo Dion. // Copyright © 2026 BrightDigit. @@ -30,7 +30,7 @@ internal import Foundation /// Command line argument parser for Swift Configuration integration -public struct CommandLineParser { +public struct CommandLineParser: Sendable { private let arguments: [String] /// Initialize with command line arguments. diff --git a/Examples/MistDemo/Sources/ConfigKeyKit/CommandRegistry.swift b/Packages/ConfigKeyKit/Sources/ConfigKeyKit/CommandRegistry.swift similarity index 98% rename from Examples/MistDemo/Sources/ConfigKeyKit/CommandRegistry.swift rename to Packages/ConfigKeyKit/Sources/ConfigKeyKit/CommandRegistry.swift index 27e836e2..06f44e38 100644 --- a/Examples/MistDemo/Sources/ConfigKeyKit/CommandRegistry.swift +++ b/Packages/ConfigKeyKit/Sources/ConfigKeyKit/CommandRegistry.swift @@ -1,6 +1,6 @@ // // CommandRegistry.swift -// MistDemo +// ConfigKeyKit // // Created by Leo Dion. // Copyright © 2026 BrightDigit. @@ -27,8 +27,6 @@ // OTHER DEALINGS IN THE SOFTWARE. // -internal import Foundation - /// Actor-based registry for managing available commands public actor CommandRegistry { /// Metadata about a command diff --git a/Examples/MistDemo/Sources/ConfigKeyKit/CommandRegistryError.swift b/Packages/ConfigKeyKit/Sources/ConfigKeyKit/CommandRegistryError.swift similarity index 98% rename from Examples/MistDemo/Sources/ConfigKeyKit/CommandRegistryError.swift rename to Packages/ConfigKeyKit/Sources/ConfigKeyKit/CommandRegistryError.swift index 51b221cb..033ae83a 100644 --- a/Examples/MistDemo/Sources/ConfigKeyKit/CommandRegistryError.swift +++ b/Packages/ConfigKeyKit/Sources/ConfigKeyKit/CommandRegistryError.swift @@ -1,6 +1,6 @@ // // CommandRegistryError.swift -// MistDemo +// ConfigKeyKit // // Created by Leo Dion. // Copyright © 2026 BrightDigit. diff --git a/Examples/MistDemo/Sources/ConfigKeyKit/ConfigKey+Bool.swift b/Packages/ConfigKeyKit/Sources/ConfigKeyKit/ConfigKey+Bool.swift similarity index 86% rename from Examples/MistDemo/Sources/ConfigKeyKit/ConfigKey+Bool.swift rename to Packages/ConfigKeyKit/Sources/ConfigKeyKit/ConfigKey+Bool.swift index eb9420d3..b30909a0 100644 --- a/Examples/MistDemo/Sources/ConfigKeyKit/ConfigKey+Bool.swift +++ b/Packages/ConfigKeyKit/Sources/ConfigKeyKit/ConfigKey+Bool.swift @@ -1,6 +1,6 @@ // // ConfigKey+Bool.swift -// MistDemo +// ConfigKeyKit // // Created by Leo Dion. // Copyright © 2026 BrightDigit. @@ -27,17 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -internal import Foundation - -// MARK: - Specialized Initializers for Booleans - extension ConfigKey where Value == Bool { - /// Non-optional default value accessor for booleans - @available(*, deprecated, message: "Use defaultValue directly instead") - public var boolDefault: Bool { - defaultValue // Already non-optional! - } - /// Initialize a boolean configuration key with non-optional default /// - Parameters: /// - cli: Command-line argument name @@ -68,5 +58,3 @@ extension ConfigKey where Value == Bool { self.defaultValue = defaultVal } } - -// Application-specific boolean key helpers should be added in application code diff --git a/Examples/MistDemo/Sources/ConfigKeyKit/ConfigKey+Debug.swift b/Packages/ConfigKeyKit/Sources/ConfigKeyKit/ConfigKey+Debug.swift similarity index 98% rename from Examples/MistDemo/Sources/ConfigKeyKit/ConfigKey+Debug.swift rename to Packages/ConfigKeyKit/Sources/ConfigKeyKit/ConfigKey+Debug.swift index f0ab9191..2f23a42b 100644 --- a/Examples/MistDemo/Sources/ConfigKeyKit/ConfigKey+Debug.swift +++ b/Packages/ConfigKeyKit/Sources/ConfigKeyKit/ConfigKey+Debug.swift @@ -1,6 +1,6 @@ // // ConfigKey+Debug.swift -// MistDemo +// ConfigKeyKit // // Created by Leo Dion. // Copyright © 2026 BrightDigit. diff --git a/Examples/MistDemo/Sources/ConfigKeyKit/ConfigKey.swift b/Packages/ConfigKeyKit/Sources/ConfigKeyKit/ConfigKey.swift similarity index 95% rename from Examples/MistDemo/Sources/ConfigKeyKit/ConfigKey.swift rename to Packages/ConfigKeyKit/Sources/ConfigKeyKit/ConfigKey.swift index a6a278f6..4115c1a8 100644 --- a/Examples/MistDemo/Sources/ConfigKeyKit/ConfigKey.swift +++ b/Packages/ConfigKeyKit/Sources/ConfigKeyKit/ConfigKey.swift @@ -1,6 +1,6 @@ // // ConfigKey.swift -// MistDemo +// ConfigKeyKit // // Created by Leo Dion. // Copyright © 2026 BrightDigit. @@ -27,10 +27,6 @@ // OTHER DEALINGS IN THE SOFTWARE. // -internal import Foundation - -// MARK: - Generic Configuration Key - /// Configuration key for values with default fallbacks /// /// Use `ConfigKey` when a configuration value has a sensible default @@ -40,7 +36,7 @@ internal import Foundation /// Example: /// ```swift /// let containerID = ConfigKey( -/// base: "cloudkit.container_id", +/// "cloudkit.container_id", /// default: "iCloud.com.example.MyApp" /// ) /// // read(containerID) returns String (non-optional) @@ -50,7 +46,7 @@ public struct ConfigKey: ConfigurationKey, Sendable { internal let styles: [ConfigKeySource: any NamingStyle] internal let explicitKeys: [ConfigKeySource: String] /// The default value returned when no source provides a value. - public let defaultValue: Value // Non-optional! + public let defaultValue: Value /// The base key string used for this configuration key public var base: String? { baseKey } diff --git a/Examples/MistDemo/Sources/ConfigKeyKit/ConfigKeySource.swift b/Packages/ConfigKeyKit/Sources/ConfigKeyKit/ConfigKeySource.swift similarity index 98% rename from Examples/MistDemo/Sources/ConfigKeyKit/ConfigKeySource.swift rename to Packages/ConfigKeyKit/Sources/ConfigKeyKit/ConfigKeySource.swift index 1adb946f..242f5108 100644 --- a/Examples/MistDemo/Sources/ConfigKeyKit/ConfigKeySource.swift +++ b/Packages/ConfigKeyKit/Sources/ConfigKeyKit/ConfigKeySource.swift @@ -1,6 +1,6 @@ // // ConfigKeySource.swift -// MistDemo +// ConfigKeyKit // // Created by Leo Dion. // Copyright © 2026 BrightDigit. diff --git a/Examples/MistDemo/Sources/ConfigKeyKit/ConfigurationKey.swift b/Packages/ConfigKeyKit/Sources/ConfigKeyKit/ConfigurationKey.swift similarity index 95% rename from Examples/MistDemo/Sources/ConfigKeyKit/ConfigurationKey.swift rename to Packages/ConfigKeyKit/Sources/ConfigKeyKit/ConfigurationKey.swift index 70458be3..a15da10a 100644 --- a/Examples/MistDemo/Sources/ConfigKeyKit/ConfigurationKey.swift +++ b/Packages/ConfigKeyKit/Sources/ConfigKeyKit/ConfigurationKey.swift @@ -1,6 +1,6 @@ // // ConfigurationKey.swift -// MistDemo +// ConfigKeyKit // // Created by Leo Dion. // Copyright © 2026 BrightDigit. @@ -27,10 +27,6 @@ // OTHER DEALINGS IN THE SOFTWARE. // -internal import Foundation - -// MARK: - Configuration Key Protocol - /// Protocol for configuration keys that support multiple sources public protocol ConfigurationKey: Sendable { /// Get the key string for a specific source diff --git a/Examples/MistDemo/Sources/ConfigKeyKit/ConfigurationParseable.swift b/Packages/ConfigKeyKit/Sources/ConfigKeyKit/ConfigurationParseable.swift similarity index 95% rename from Examples/MistDemo/Sources/ConfigKeyKit/ConfigurationParseable.swift rename to Packages/ConfigKeyKit/Sources/ConfigKeyKit/ConfigurationParseable.swift index 417b1b24..393bcba1 100644 --- a/Examples/MistDemo/Sources/ConfigKeyKit/ConfigurationParseable.swift +++ b/Packages/ConfigKeyKit/Sources/ConfigKeyKit/ConfigurationParseable.swift @@ -1,6 +1,6 @@ // // ConfigurationParseable.swift -// MistDemo +// ConfigKeyKit // // Created by Leo Dion. // Copyright © 2026 BrightDigit. @@ -27,8 +27,6 @@ // OTHER DEALINGS IN THE SOFTWARE. // -internal import Foundation - /// Protocol for configuration types that can parse themselves /// from command line arguments and environment variables. public protocol ConfigurationParseable: Sendable { @@ -47,7 +45,6 @@ public protocol ConfigurationParseable: Sendable { init(configuration: ConfigReader, base: BaseConfig?) async throws } -/// Extension for root configurations (where BaseConfig == Never) extension ConfigurationParseable where BaseConfig == Never { /// Convenience initializer for root configs that don't need a parent public init(configuration: ConfigReader) async throws { diff --git a/Examples/MistDemo/Sources/ConfigKeyKit/NamingStyle.swift b/Packages/ConfigKeyKit/Sources/ConfigKeyKit/NamingStyle.swift similarity index 98% rename from Examples/MistDemo/Sources/ConfigKeyKit/NamingStyle.swift rename to Packages/ConfigKeyKit/Sources/ConfigKeyKit/NamingStyle.swift index bb72ddd8..6df8614f 100644 --- a/Examples/MistDemo/Sources/ConfigKeyKit/NamingStyle.swift +++ b/Packages/ConfigKeyKit/Sources/ConfigKeyKit/NamingStyle.swift @@ -1,6 +1,6 @@ // // NamingStyle.swift -// MistDemo +// ConfigKeyKit // // Created by Leo Dion. // Copyright © 2026 BrightDigit. diff --git a/Examples/MistDemo/Sources/ConfigKeyKit/OptionalConfigKey+Debug.swift b/Packages/ConfigKeyKit/Sources/ConfigKeyKit/OptionalConfigKey+Debug.swift similarity index 98% rename from Examples/MistDemo/Sources/ConfigKeyKit/OptionalConfigKey+Debug.swift rename to Packages/ConfigKeyKit/Sources/ConfigKeyKit/OptionalConfigKey+Debug.swift index a15a0079..b1e01471 100644 --- a/Examples/MistDemo/Sources/ConfigKeyKit/OptionalConfigKey+Debug.swift +++ b/Packages/ConfigKeyKit/Sources/ConfigKeyKit/OptionalConfigKey+Debug.swift @@ -1,6 +1,6 @@ // // OptionalConfigKey+Debug.swift -// MistDemo +// ConfigKeyKit // // Created by Leo Dion. // Copyright © 2026 BrightDigit. diff --git a/Examples/MistDemo/Sources/ConfigKeyKit/OptionalConfigKey.swift b/Packages/ConfigKeyKit/Sources/ConfigKeyKit/OptionalConfigKey.swift similarity index 96% rename from Examples/MistDemo/Sources/ConfigKeyKit/OptionalConfigKey.swift rename to Packages/ConfigKeyKit/Sources/ConfigKeyKit/OptionalConfigKey.swift index 0a80c71d..23b63d46 100644 --- a/Examples/MistDemo/Sources/ConfigKeyKit/OptionalConfigKey.swift +++ b/Packages/ConfigKeyKit/Sources/ConfigKeyKit/OptionalConfigKey.swift @@ -1,6 +1,6 @@ // // OptionalConfigKey.swift -// MistDemo +// ConfigKeyKit // // Created by Leo Dion. // Copyright © 2026 BrightDigit. @@ -27,10 +27,6 @@ // OTHER DEALINGS IN THE SOFTWARE. // -internal import Foundation - -// MARK: - Optional Configuration Key - /// Configuration key for optional values without defaults /// /// Use `OptionalConfigKey` when a configuration value has no sensible default @@ -39,7 +35,7 @@ internal import Foundation /// /// Example: /// ```swift -/// let apiKey = OptionalConfigKey(base: "api.key") +/// let apiKey = OptionalConfigKey("api.key") /// // read(apiKey) returns String? /// ``` public struct OptionalConfigKey: ConfigurationKey, Sendable { diff --git a/Examples/MistDemo/Sources/ConfigKeyKit/StandardNamingStyle.swift b/Packages/ConfigKeyKit/Sources/ConfigKeyKit/StandardNamingStyle.swift similarity index 94% rename from Examples/MistDemo/Sources/ConfigKeyKit/StandardNamingStyle.swift rename to Packages/ConfigKeyKit/Sources/ConfigKeyKit/StandardNamingStyle.swift index 5b626df8..faee3b8d 100644 --- a/Examples/MistDemo/Sources/ConfigKeyKit/StandardNamingStyle.swift +++ b/Packages/ConfigKeyKit/Sources/ConfigKeyKit/StandardNamingStyle.swift @@ -1,6 +1,6 @@ // // StandardNamingStyle.swift -// MistDemo +// ConfigKeyKit // // Created by Leo Dion. // Copyright © 2026 BrightDigit. @@ -34,7 +34,7 @@ public enum StandardNamingStyle: NamingStyle, Sendable { /// Dot-separated lowercase (e.g., "cloudkit.container_id") case dotSeparated - /// Screaming snake case with prefix (e.g., "APP_CLOUDKIT_CONTAINER_ID") + /// Screaming snake case with optional prefix (e.g., "APP_CLOUDKIT_CONTAINER_ID") case screamingSnakeCase(prefix: String?) /// Transform the base key string into the appropriate naming style. diff --git a/Examples/MistDemo/Tests/MistDemoTests/ConfigKeyKit/CommandLineParserTests.swift b/Packages/ConfigKeyKit/Tests/ConfigKeyKitTests/CommandLineParserTests.swift similarity index 85% rename from Examples/MistDemo/Tests/MistDemoTests/ConfigKeyKit/CommandLineParserTests.swift rename to Packages/ConfigKeyKit/Tests/ConfigKeyKitTests/CommandLineParserTests.swift index d80cd19c..d01e4a4c 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/ConfigKeyKit/CommandLineParserTests.swift +++ b/Packages/ConfigKeyKit/Tests/ConfigKeyKitTests/CommandLineParserTests.swift @@ -1,6 +1,6 @@ // // CommandLineParserTests.swift -// MistDemoTests +// ConfigKeyKit // // Created by Leo Dion. // Copyright © 2026 BrightDigit. @@ -35,7 +35,7 @@ internal import Testing internal struct CommandLineParserTests { @Test("parseCommandName returns nil when only executable name is present") internal func noArgsReturnsNil() { - let parser = CommandLineParser(arguments: ["mistdemo"]) + let parser = CommandLineParser(arguments: ["mytool"]) #expect(parser.parseCommandName() == nil) #expect(parser.commandArguments().isEmpty) @@ -44,7 +44,7 @@ internal struct CommandLineParserTests { @Test("parseCommandName returns the first non-option argument") internal func parsesCommand() { - let parser = CommandLineParser(arguments: ["mistdemo", "query", "--limit", "10"]) + let parser = CommandLineParser(arguments: ["mytool", "query", "--limit", "10"]) #expect(parser.parseCommandName() == "query") #expect(parser.commandArguments() == ["--limit", "10"]) @@ -52,7 +52,7 @@ internal struct CommandLineParserTests { @Test("parseCommandName returns nil when first argument is a global option") internal func globalOptionReturnsNilCommand() { - let parser = CommandLineParser(arguments: ["mistdemo", "--config-file", "/tmp/x.json"]) + let parser = CommandLineParser(arguments: ["mytool", "--config-file", "/tmp/x.json"]) #expect(parser.parseCommandName() == nil) #expect(parser.commandArguments() == ["--config-file", "/tmp/x.json"]) @@ -60,7 +60,7 @@ internal struct CommandLineParserTests { @Test("commandArguments strips the executable + command but keeps the rest verbatim") internal func commandArgumentsPreserveRest() { - let parser = CommandLineParser(arguments: ["mistdemo", "lookup", "rec-1", "rec-2"]) + let parser = CommandLineParser(arguments: ["mytool", "lookup", "rec-1", "rec-2"]) #expect(parser.commandArguments() == ["rec-1", "rec-2"]) } @@ -70,7 +70,7 @@ internal struct CommandLineParserTests { arguments: ["--help", "-h", "help"] ) internal func helpTokens(token: String) { - let parser = CommandLineParser(arguments: ["mistdemo", "query", token]) + let parser = CommandLineParser(arguments: ["mytool", "query", token]) #expect(parser.isHelpRequested() == true) } diff --git a/Examples/MistDemo/Tests/MistDemoTests/ConfigKeyKit/CommandRegistry/CommandRegistryTests+AvailableCommands.swift b/Packages/ConfigKeyKit/Tests/ConfigKeyKitTests/CommandRegistry/CommandRegistry+AvailableCommands.swift similarity index 78% rename from Examples/MistDemo/Tests/MistDemoTests/ConfigKeyKit/CommandRegistry/CommandRegistryTests+AvailableCommands.swift rename to Packages/ConfigKeyKit/Tests/ConfigKeyKitTests/CommandRegistry/CommandRegistry+AvailableCommands.swift index 3a8e0582..5d081211 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/ConfigKeyKit/CommandRegistry/CommandRegistryTests+AvailableCommands.swift +++ b/Packages/ConfigKeyKit/Tests/ConfigKeyKitTests/CommandRegistry/CommandRegistry+AvailableCommands.swift @@ -1,6 +1,6 @@ // -// CommandRegistryTests+AvailableCommands.swift -// MistDemo +// CommandRegistry+AvailableCommands.swift +// ConfigKeyKit // // Created by Leo Dion. // Copyright © 2026 BrightDigit. @@ -27,20 +27,19 @@ // OTHER DEALINGS IN THE SOFTWARE. // -internal import Foundation -internal import Testing +import Testing @testable import ConfigKeyKit -extension CommandRegistryTests { +extension CommandRegistry { @Suite("Available Commands") internal struct AvailableCommands { @Test("Available commands lists registered commands") internal func availableCommands() async { - let registry = CommandRegistry() + let registry = ConfigKeyKit.CommandRegistry() - await registry.register(CommandRegistryTests.TestCommand.self) - await registry.register(CommandRegistryTests.AnotherCommand.self) + await registry.register(CommandRegistry.TestCommand.self) + await registry.register(CommandRegistry.AnotherCommand.self) let commands = await registry.availableCommands @@ -51,7 +50,7 @@ extension CommandRegistryTests { @Test("Available commands returns empty for new registry") internal func availableCommandsEmpty() async { - let registry = CommandRegistry() + let registry = ConfigKeyKit.CommandRegistry() let commands = await registry.availableCommands @@ -60,10 +59,10 @@ extension CommandRegistryTests { @Test("Available commands are sorted") internal func availableCommandsSorted() async { - let registry = CommandRegistry() + let registry = ConfigKeyKit.CommandRegistry() - await registry.register(CommandRegistryTests.AnotherCommand.self) - await registry.register(CommandRegistryTests.TestCommand.self) + await registry.register(CommandRegistry.AnotherCommand.self) + await registry.register(CommandRegistry.TestCommand.self) let commands = await registry.availableCommands diff --git a/Examples/MistDemo/Tests/MistDemoTests/ConfigKeyKit/CommandRegistry/CommandRegistryTests+CommandCreation.swift b/Packages/ConfigKeyKit/Tests/ConfigKeyKitTests/CommandRegistry/CommandRegistry+CommandCreation.swift similarity index 82% rename from Examples/MistDemo/Tests/MistDemoTests/ConfigKeyKit/CommandRegistry/CommandRegistryTests+CommandCreation.swift rename to Packages/ConfigKeyKit/Tests/ConfigKeyKitTests/CommandRegistry/CommandRegistry+CommandCreation.swift index 7bdc4bc7..f732ceb2 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/ConfigKeyKit/CommandRegistry/CommandRegistryTests+CommandCreation.swift +++ b/Packages/ConfigKeyKit/Tests/ConfigKeyKitTests/CommandRegistry/CommandRegistry+CommandCreation.swift @@ -1,6 +1,6 @@ // -// CommandRegistryTests+CommandCreation.swift -// MistDemo +// CommandRegistry+CommandCreation.swift +// ConfigKeyKit // // Created by Leo Dion. // Copyright © 2026 BrightDigit. @@ -27,28 +27,27 @@ // OTHER DEALINGS IN THE SOFTWARE. // -internal import Foundation -internal import Testing +import Testing @testable import ConfigKeyKit -extension CommandRegistryTests { +extension CommandRegistry { @Suite("Command Creation") internal struct CommandCreation { @Test("Create command instance") internal func createCommandInstance() async throws { - let registry = CommandRegistry() + let registry = ConfigKeyKit.CommandRegistry() - await registry.register(CommandRegistryTests.TestCommand.self) + await registry.register(CommandRegistry.TestCommand.self) let command = try await registry.createCommand(named: "test") - #expect(command is CommandRegistryTests.TestCommand) + #expect(command is CommandRegistry.TestCommand) } @Test("Create command instance throws for unknown command") internal func createCommandInstanceThrows() async { - let registry = CommandRegistry() + let registry = ConfigKeyKit.CommandRegistry() await #expect(throws: CommandRegistryError.self) { try await registry.createCommand(named: "unknown") diff --git a/Examples/MistDemo/Tests/MistDemoTests/ConfigKeyKit/CommandRegistry/CommandRegistryTests+CommandTypeRetrieval.swift b/Packages/ConfigKeyKit/Tests/ConfigKeyKitTests/CommandRegistry/CommandRegistry+CommandTypeRetrieval.swift similarity index 85% rename from Examples/MistDemo/Tests/MistDemoTests/ConfigKeyKit/CommandRegistry/CommandRegistryTests+CommandTypeRetrieval.swift rename to Packages/ConfigKeyKit/Tests/ConfigKeyKitTests/CommandRegistry/CommandRegistry+CommandTypeRetrieval.swift index b485e82c..cff425d8 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/ConfigKeyKit/CommandRegistry/CommandRegistryTests+CommandTypeRetrieval.swift +++ b/Packages/ConfigKeyKit/Tests/ConfigKeyKitTests/CommandRegistry/CommandRegistry+CommandTypeRetrieval.swift @@ -1,6 +1,6 @@ // -// CommandRegistryTests+CommandTypeRetrieval.swift -// MistDemo +// CommandRegistry+CommandTypeRetrieval.swift +// ConfigKeyKit // // Created by Leo Dion. // Copyright © 2026 BrightDigit. @@ -27,19 +27,18 @@ // OTHER DEALINGS IN THE SOFTWARE. // -internal import Foundation -internal import Testing +import Testing @testable import ConfigKeyKit -extension CommandRegistryTests { +extension CommandRegistry { @Suite("Command Type Retrieval") internal struct CommandTypeRetrieval { @Test("Get command type by name") internal func getCommandType() async { - let registry = CommandRegistry() + let registry = ConfigKeyKit.CommandRegistry() - await registry.register(CommandRegistryTests.TestCommand.self) + await registry.register(CommandRegistry.TestCommand.self) let commandType = await registry.commandType(named: "test") @@ -48,7 +47,7 @@ extension CommandRegistryTests { @Test("Get command type for unregistered command") internal func getCommandTypeUnregistered() async { - let registry = CommandRegistry() + let registry = ConfigKeyKit.CommandRegistry() let commandType = await registry.commandType(named: "nonexistent") diff --git a/Examples/MistDemo/Tests/MistDemoTests/ConfigKeyKit/CommandRegistry/CommandRegistryTests+ConcurrentAccess.swift b/Packages/ConfigKeyKit/Tests/ConfigKeyKitTests/CommandRegistry/CommandRegistry+ConcurrentAccess.swift similarity index 81% rename from Examples/MistDemo/Tests/MistDemoTests/ConfigKeyKit/CommandRegistry/CommandRegistryTests+ConcurrentAccess.swift rename to Packages/ConfigKeyKit/Tests/ConfigKeyKitTests/CommandRegistry/CommandRegistry+ConcurrentAccess.swift index 47d7d990..7f722614 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/ConfigKeyKit/CommandRegistry/CommandRegistryTests+ConcurrentAccess.swift +++ b/Packages/ConfigKeyKit/Tests/ConfigKeyKitTests/CommandRegistry/CommandRegistry+ConcurrentAccess.swift @@ -1,6 +1,6 @@ // -// CommandRegistryTests+ConcurrentAccess.swift -// MistDemo +// CommandRegistry+ConcurrentAccess.swift +// ConfigKeyKit // // Created by Leo Dion. // Copyright © 2026 BrightDigit. @@ -27,24 +27,23 @@ // OTHER DEALINGS IN THE SOFTWARE. // -internal import Foundation -internal import Testing +import Testing @testable import ConfigKeyKit -extension CommandRegistryTests { +extension CommandRegistry { @Suite("Concurrent Access") internal struct ConcurrentAccess { @Test("Concurrent registration") internal func concurrentRegistration() async { - let registry = CommandRegistry() + let registry = ConfigKeyKit.CommandRegistry() await withTaskGroup(of: Void.self) { group in group.addTask { - await registry.register(CommandRegistryTests.TestCommand.self) + await registry.register(CommandRegistry.TestCommand.self) } group.addTask { - await registry.register(CommandRegistryTests.AnotherCommand.self) + await registry.register(CommandRegistry.AnotherCommand.self) } } @@ -54,9 +53,9 @@ extension CommandRegistryTests { @Test("Concurrent reads") internal func concurrentReads() async { - let registry = CommandRegistry() + let registry = ConfigKeyKit.CommandRegistry() - await registry.register(CommandRegistryTests.TestCommand.self) + await registry.register(CommandRegistry.TestCommand.self) await withTaskGroup(of: Bool.self) { group in for _ in 0..<10 { @@ -76,11 +75,11 @@ extension CommandRegistryTests { @Test("Mixed concurrent operations") internal func mixedConcurrentOperations() async { - let registry = CommandRegistry() + let registry = ConfigKeyKit.CommandRegistry() await withTaskGroup(of: Void.self) { group in group.addTask { - await registry.register(CommandRegistryTests.TestCommand.self) + await registry.register(CommandRegistry.TestCommand.self) } group.addTask { _ = await registry.isRegistered("test") diff --git a/Examples/MistDemo/Tests/MistDemoTests/ConfigKeyKit/CommandRegistry/CommandRegistryTests+Errors.swift b/Packages/ConfigKeyKit/Tests/ConfigKeyKitTests/CommandRegistry/CommandRegistry+Errors.swift similarity index 95% rename from Examples/MistDemo/Tests/MistDemoTests/ConfigKeyKit/CommandRegistry/CommandRegistryTests+Errors.swift rename to Packages/ConfigKeyKit/Tests/ConfigKeyKitTests/CommandRegistry/CommandRegistry+Errors.swift index bc6b10c8..cce54203 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/ConfigKeyKit/CommandRegistry/CommandRegistryTests+Errors.swift +++ b/Packages/ConfigKeyKit/Tests/ConfigKeyKitTests/CommandRegistry/CommandRegistry+Errors.swift @@ -1,6 +1,6 @@ // -// CommandRegistryTests+Errors.swift -// MistDemo +// CommandRegistry+Errors.swift +// ConfigKeyKit // // Created by Leo Dion. // Copyright © 2026 BrightDigit. @@ -32,7 +32,7 @@ internal import Testing @testable import ConfigKeyKit -extension CommandRegistryTests { +extension CommandRegistry { @Suite("Errors") internal struct Errors { @Test("Unknown command error has description") diff --git a/Examples/MistDemo/Tests/MistDemoTests/ConfigKeyKit/CommandRegistry/CommandRegistryTests+Metadata.swift b/Packages/ConfigKeyKit/Tests/ConfigKeyKitTests/CommandRegistry/CommandRegistry+Metadata.swift similarity index 86% rename from Examples/MistDemo/Tests/MistDemoTests/ConfigKeyKit/CommandRegistry/CommandRegistryTests+Metadata.swift rename to Packages/ConfigKeyKit/Tests/ConfigKeyKitTests/CommandRegistry/CommandRegistry+Metadata.swift index 19806ad1..f12242ed 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/ConfigKeyKit/CommandRegistry/CommandRegistryTests+Metadata.swift +++ b/Packages/ConfigKeyKit/Tests/ConfigKeyKitTests/CommandRegistry/CommandRegistry+Metadata.swift @@ -1,6 +1,6 @@ // -// CommandRegistryTests+Metadata.swift -// MistDemo +// CommandRegistry+Metadata.swift +// ConfigKeyKit // // Created by Leo Dion. // Copyright © 2026 BrightDigit. @@ -27,19 +27,18 @@ // OTHER DEALINGS IN THE SOFTWARE. // -internal import Foundation -internal import Testing +import Testing @testable import ConfigKeyKit -extension CommandRegistryTests { +extension CommandRegistry { @Suite("Metadata") internal struct Metadata { @Test("Get command metadata") internal func getCommandMetadata() async { - let registry = CommandRegistry() + let registry = ConfigKeyKit.CommandRegistry() - await registry.register(CommandRegistryTests.TestCommand.self) + await registry.register(CommandRegistry.TestCommand.self) let metadata = await registry.metadata(for: "test") @@ -51,7 +50,7 @@ extension CommandRegistryTests { @Test("Get metadata for unregistered command") internal func getMetadataForUnregistered() async { - let registry = CommandRegistry() + let registry = ConfigKeyKit.CommandRegistry() let metadata = await registry.metadata(for: "nonexistent") diff --git a/Examples/MistDemo/Tests/MistDemoTests/ConfigKeyKit/CommandRegistry/CommandRegistryTests+Overwrite.swift b/Packages/ConfigKeyKit/Tests/ConfigKeyKitTests/CommandRegistry/CommandRegistry+Overwrite.swift similarity index 82% rename from Examples/MistDemo/Tests/MistDemoTests/ConfigKeyKit/CommandRegistry/CommandRegistryTests+Overwrite.swift rename to Packages/ConfigKeyKit/Tests/ConfigKeyKitTests/CommandRegistry/CommandRegistry+Overwrite.swift index 487e7931..c77bde00 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/ConfigKeyKit/CommandRegistry/CommandRegistryTests+Overwrite.swift +++ b/Packages/ConfigKeyKit/Tests/ConfigKeyKitTests/CommandRegistry/CommandRegistry+Overwrite.swift @@ -1,6 +1,6 @@ // -// CommandRegistryTests+Overwrite.swift -// MistDemo +// CommandRegistry+Overwrite.swift +// ConfigKeyKit // // Created by Leo Dion. // Copyright © 2026 BrightDigit. @@ -27,20 +27,19 @@ // OTHER DEALINGS IN THE SOFTWARE. // -internal import Foundation -internal import Testing +import Testing @testable import ConfigKeyKit -extension CommandRegistryTests { +extension CommandRegistry { @Suite("Overwrite") internal struct Overwrite { @Test("Registering same command twice overwrites") internal func registerCommandTwiceOverwrites() async { - let registry = CommandRegistry() + let registry = ConfigKeyKit.CommandRegistry() - await registry.register(CommandRegistryTests.TestCommand.self) - await registry.register(CommandRegistryTests.TestCommand.self) + await registry.register(CommandRegistry.TestCommand.self) + await registry.register(CommandRegistry.TestCommand.self) let commands = await registry.availableCommands #expect(commands.count == 1) diff --git a/Examples/MistDemo/Tests/MistDemoTests/ConfigKeyKit/CommandRegistry/CommandRegistryTests+Registration.swift b/Packages/ConfigKeyKit/Tests/ConfigKeyKitTests/CommandRegistry/CommandRegistry+Registration.swift similarity index 80% rename from Examples/MistDemo/Tests/MistDemoTests/ConfigKeyKit/CommandRegistry/CommandRegistryTests+Registration.swift rename to Packages/ConfigKeyKit/Tests/ConfigKeyKitTests/CommandRegistry/CommandRegistry+Registration.swift index ccf99b02..8c86972b 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/ConfigKeyKit/CommandRegistry/CommandRegistryTests+Registration.swift +++ b/Packages/ConfigKeyKit/Tests/ConfigKeyKitTests/CommandRegistry/CommandRegistry+Registration.swift @@ -1,6 +1,6 @@ // -// CommandRegistryTests+Registration.swift -// MistDemo +// CommandRegistry+Registration.swift +// ConfigKeyKit // // Created by Leo Dion. // Copyright © 2026 BrightDigit. @@ -27,19 +27,18 @@ // OTHER DEALINGS IN THE SOFTWARE. // -internal import Foundation -internal import Testing +import Testing @testable import ConfigKeyKit -extension CommandRegistryTests { +extension CommandRegistry { @Suite("Registration") internal struct Registration { @Test("Register a command") internal func registerCommand() async { - let registry = CommandRegistry() + let registry = ConfigKeyKit.CommandRegistry() - await registry.register(CommandRegistryTests.TestCommand.self) + await registry.register(CommandRegistry.TestCommand.self) let isRegistered = await registry.isRegistered("test") #expect(isRegistered == true) @@ -47,10 +46,10 @@ extension CommandRegistryTests { @Test("Register multiple commands") internal func registerMultipleCommands() async { - let registry = CommandRegistry() + let registry = ConfigKeyKit.CommandRegistry() - await registry.register(CommandRegistryTests.TestCommand.self) - await registry.register(CommandRegistryTests.AnotherCommand.self) + await registry.register(CommandRegistry.TestCommand.self) + await registry.register(CommandRegistry.AnotherCommand.self) let testRegistered = await registry.isRegistered("test") let anotherRegistered = await registry.isRegistered("another") @@ -61,7 +60,7 @@ extension CommandRegistryTests { @Test("Unregistered command returns false") internal func unregisteredCommand() async { - let registry = CommandRegistry() + let registry = ConfigKeyKit.CommandRegistry() let isRegistered = await registry.isRegistered("nonexistent") #expect(isRegistered == false) diff --git a/Examples/MistDemo/Tests/MistDemoTests/ConfigKeyKit/CommandRegistry/CommandRegistryTests+TestCommandTypes.swift b/Packages/ConfigKeyKit/Tests/ConfigKeyKitTests/CommandRegistry/CommandRegistry+TestCommandTypes.swift similarity index 94% rename from Examples/MistDemo/Tests/MistDemoTests/ConfigKeyKit/CommandRegistry/CommandRegistryTests+TestCommandTypes.swift rename to Packages/ConfigKeyKit/Tests/ConfigKeyKitTests/CommandRegistry/CommandRegistry+TestCommandTypes.swift index 1d86a994..e62d3bc5 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/ConfigKeyKit/CommandRegistry/CommandRegistryTests+TestCommandTypes.swift +++ b/Packages/ConfigKeyKit/Tests/ConfigKeyKitTests/CommandRegistry/CommandRegistry+TestCommandTypes.swift @@ -1,6 +1,6 @@ // -// CommandRegistryTests+TestCommandTypes.swift -// MistDemo +// CommandRegistry+TestCommandTypes.swift +// ConfigKeyKit // // Created by Leo Dion. // Copyright © 2026 BrightDigit. @@ -27,11 +27,9 @@ // OTHER DEALINGS IN THE SOFTWARE. // -internal import Foundation - @testable import ConfigKeyKit -extension CommandRegistryTests { +extension CommandRegistry { internal struct TestCommand: Command { internal typealias Config = TestConfig @@ -81,7 +79,7 @@ extension CommandRegistryTests { } } - internal struct TestConfigReader { + internal struct TestConfigReader: Sendable { // Minimal config reader for testing } } diff --git a/Examples/MistDemo/Tests/MistDemoTests/ConfigKeyKit/CommandRegistry/CommandRegistryTests.swift b/Packages/ConfigKeyKit/Tests/ConfigKeyKitTests/CommandRegistry/CommandRegistry.swift similarity index 82% rename from Examples/MistDemo/Tests/MistDemoTests/ConfigKeyKit/CommandRegistry/CommandRegistryTests.swift rename to Packages/ConfigKeyKit/Tests/ConfigKeyKitTests/CommandRegistry/CommandRegistry.swift index 68712d1c..e55378f2 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/ConfigKeyKit/CommandRegistry/CommandRegistryTests.swift +++ b/Packages/ConfigKeyKit/Tests/ConfigKeyKitTests/CommandRegistry/CommandRegistry.swift @@ -1,6 +1,6 @@ // -// CommandRegistryTests.swift -// MistDemo +// CommandRegistry.swift +// ConfigKeyKit // // Created by Leo Dion. // Copyright © 2026 BrightDigit. @@ -29,5 +29,11 @@ internal import Testing -@Suite("CommandRegistry") -internal enum CommandRegistryTests {} +@Suite( + "CommandRegistry", + .disabled( + if: TestEnvironment.hangsOnTestRunning, + "Windows + Swift 6.2: async/actor tests hang the Swift Testing runner mid-flight" + ) +) +internal enum CommandRegistry {} diff --git a/Packages/ConfigKeyKit/Tests/ConfigKeyKitTests/ConfigKeySourceTests.swift b/Packages/ConfigKeyKit/Tests/ConfigKeyKitTests/ConfigKeySourceTests.swift new file mode 100644 index 00000000..48eab164 --- /dev/null +++ b/Packages/ConfigKeyKit/Tests/ConfigKeyKitTests/ConfigKeySourceTests.swift @@ -0,0 +1,43 @@ +// +// ConfigKeySourceTests.swift +// ConfigKeyKit +// +// Created by Leo Dion. +// Copyright © 2026 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +import Testing + +@testable import ConfigKeyKit + +@Suite("ConfigKeySource Tests") +internal struct ConfigKeySourceTests { + @Test("All cases") + internal func allCases() { + let sources = ConfigKeySource.allCases + #expect(sources.count == 2) + #expect(sources.contains(.commandLine)) + #expect(sources.contains(.environment)) + } +} diff --git a/Packages/ConfigKeyKit/Tests/ConfigKeyKitTests/ConfigKeyTests.swift b/Packages/ConfigKeyKit/Tests/ConfigKeyKitTests/ConfigKeyTests.swift new file mode 100644 index 00000000..79daeca1 --- /dev/null +++ b/Packages/ConfigKeyKit/Tests/ConfigKeyKitTests/ConfigKeyTests.swift @@ -0,0 +1,83 @@ +// +// ConfigKeyTests.swift +// ConfigKeyKit +// +// Created by Leo Dion. +// Copyright © 2026 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +import Testing + +@testable import ConfigKeyKit + +@Suite("ConfigKey Tests") +internal struct ConfigKeyTests { + @Test("ConfigKey with explicit keys and default") + internal func explicitKeys() { + let key = ConfigKey(cli: "test.key", env: "TEST_KEY", default: "default-value") + + #expect(key.key(for: .commandLine) == "test.key") + #expect(key.key(for: .environment) == "TEST_KEY") + #expect(key.defaultValue == "default-value") + } + + @Test("ConfigKey with base string and custom env prefix") + internal func baseStringWithCustomPrefix() { + let key = ConfigKey( + "cloudkit.container_id", envPrefix: "MYAPP", default: "iCloud.com.example.App" + ) + + #expect(key.key(for: .commandLine) == "cloudkit.container_id") + #expect(key.key(for: .environment) == "MYAPP_CLOUDKIT_CONTAINER_ID") + #expect(key.defaultValue == "iCloud.com.example.App") + } + + @Test("ConfigKey with base string and no prefix") + internal func baseStringNoPrefix() { + let key = ConfigKey( + "cloudkit.container_id", envPrefix: nil, default: "iCloud.com.example.App" + ) + + #expect(key.key(for: .commandLine) == "cloudkit.container_id") + #expect(key.key(for: .environment) == "CLOUDKIT_CONTAINER_ID") + #expect(key.defaultValue == "iCloud.com.example.App") + } + + @Test("ConfigKey exposes its base string") + internal func baseAccessor() { + let withBase = ConfigKey("cloudkit.container_id", default: "default") + let withoutBase = ConfigKey(cli: "x", env: "X", default: "default") + + #expect(withBase.base == "cloudkit.container_id") + #expect(withoutBase.base == nil) + } + + @Test("Boolean ConfigKey with default") + internal func booleanDefaultValue() { + let key = ConfigKey("sync.verbose", envPrefix: "MYAPP", default: false) + + #expect(key.defaultValue == false) + #expect(key.key(for: .environment) == "MYAPP_SYNC_VERBOSE") + } +} diff --git a/Packages/ConfigKeyKit/Tests/ConfigKeyKitTests/NamingStyleTests.swift b/Packages/ConfigKeyKit/Tests/ConfigKeyKitTests/NamingStyleTests.swift new file mode 100644 index 00000000..9969a08e --- /dev/null +++ b/Packages/ConfigKeyKit/Tests/ConfigKeyKitTests/NamingStyleTests.swift @@ -0,0 +1,59 @@ +// +// NamingStyleTests.swift +// ConfigKeyKit +// +// Created by Leo Dion. +// Copyright © 2026 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +import Testing + +@testable import ConfigKeyKit + +@Suite("NamingStyle Tests") +internal struct NamingStyleTests { + @Test("Dot-separated style") + internal func dotSeparatedStyle() { + let style = StandardNamingStyle.dotSeparated + #expect(style.transform("cloudkit.container_id") == "cloudkit.container_id") + } + + @Test("Screaming snake case with prefix") + internal func screamingSnakeCaseWithPrefix() { + let style = StandardNamingStyle.screamingSnakeCase(prefix: "MYAPP") + #expect(style.transform("cloudkit.container_id") == "MYAPP_CLOUDKIT_CONTAINER_ID") + } + + @Test("Screaming snake case without prefix") + internal func screamingSnakeCaseNoPrefix() { + let style = StandardNamingStyle.screamingSnakeCase(prefix: nil) + #expect(style.transform("cloudkit.container_id") == "CLOUDKIT_CONTAINER_ID") + } + + @Test("Screaming snake case with nil prefix on shorter key") + internal func screamingSnakeCaseNilPrefixShort() { + let style = StandardNamingStyle.screamingSnakeCase(prefix: nil) + #expect(style.transform("sync.verbose") == "SYNC_VERBOSE") + } +} diff --git a/Packages/ConfigKeyKit/Tests/ConfigKeyKitTests/OptionalConfigKeyTests.swift b/Packages/ConfigKeyKit/Tests/ConfigKeyKitTests/OptionalConfigKeyTests.swift new file mode 100644 index 00000000..945f622b --- /dev/null +++ b/Packages/ConfigKeyKit/Tests/ConfigKeyKitTests/OptionalConfigKeyTests.swift @@ -0,0 +1,84 @@ +// +// OptionalConfigKeyTests.swift +// ConfigKeyKit +// +// Created by Leo Dion. +// Copyright © 2026 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +import Testing + +@testable import ConfigKeyKit + +@Suite("OptionalConfigKey Tests") +internal struct OptionalConfigKeyTests { + @Test("OptionalConfigKey with explicit keys") + internal func explicitKeys() { + let key = OptionalConfigKey(cli: "test.key", env: "TEST_KEY") + + #expect(key.key(for: .commandLine) == "test.key") + #expect(key.key(for: .environment) == "TEST_KEY") + } + + @Test("OptionalConfigKey with base string and custom env prefix") + internal func baseStringWithCustomPrefix() { + let key = OptionalConfigKey("cloudkit.key_id", envPrefix: "MYAPP") + + #expect(key.key(for: .commandLine) == "cloudkit.key_id") + #expect(key.key(for: .environment) == "MYAPP_CLOUDKIT_KEY_ID") + } + + @Test("OptionalConfigKey with base string and no prefix") + internal func baseStringNoPrefix() { + let key = OptionalConfigKey("cloudkit.key_id", envPrefix: nil) + + #expect(key.key(for: .commandLine) == "cloudkit.key_id") + #expect(key.key(for: .environment) == "CLOUDKIT_KEY_ID") + } + + @Test("OptionalConfigKey and ConfigKey generate identical keys") + internal func keyGenerationParity() { + let optional = OptionalConfigKey("test.key", envPrefix: "MYAPP") + let withDefault = ConfigKey("test.key", envPrefix: "MYAPP", default: "default") + + #expect(optional.key(for: .commandLine) == withDefault.key(for: .commandLine)) + #expect(optional.key(for: .environment) == withDefault.key(for: .environment)) + } + + @Test("OptionalConfigKey for Int type") + internal func intOptionalKey() { + let key = OptionalConfigKey("sync.min_interval", envPrefix: "MYAPP") + + #expect(key.key(for: .commandLine) == "sync.min_interval") + #expect(key.key(for: .environment) == "MYAPP_SYNC_MIN_INTERVAL") + } + + @Test("OptionalConfigKey for Double type") + internal func doubleOptionalKey() { + let key = OptionalConfigKey("fetch.interval_global", envPrefix: "MYAPP") + + #expect(key.key(for: .commandLine) == "fetch.interval_global") + #expect(key.key(for: .environment) == "MYAPP_FETCH_INTERVAL_GLOBAL") + } +} diff --git a/Packages/ConfigKeyKit/Tests/ConfigKeyKitTests/TestEnvironment.swift b/Packages/ConfigKeyKit/Tests/ConfigKeyKitTests/TestEnvironment.swift new file mode 100644 index 00000000..c44b2684 --- /dev/null +++ b/Packages/ConfigKeyKit/Tests/ConfigKeyKitTests/TestEnvironment.swift @@ -0,0 +1,16 @@ +// +// TestEnvironment.swift +// ConfigKeyKit +// +// Created by Leo Dion on 5/19/26. +// + +internal enum TestEnvironment { + internal static let hangsOnTestRunning: Bool = { + #if os(Windows) && swift(<6.3) + return true + #else + return false + #endif + }() +} diff --git a/Packages/ConfigKeyKit/codecov.yml b/Packages/ConfigKeyKit/codecov.yml new file mode 100644 index 00000000..d07d53ee --- /dev/null +++ b/Packages/ConfigKeyKit/codecov.yml @@ -0,0 +1,9 @@ +coverage: + status: + patch: + default: + target: auto + threshold: 2% + +ignore: + - "Tests" diff --git a/Packages/ConfigKeyKit/mise.toml b/Packages/ConfigKeyKit/mise.toml new file mode 100644 index 00000000..6df20abb --- /dev/null +++ b/Packages/ConfigKeyKit/mise.toml @@ -0,0 +1,7 @@ +[settings] +experimental = true + +[tools] +"spm:swiftlang/swift-format" = "602.0.0" +"aqua:realm/SwiftLint" = "0.62.2" +"spm:peripheryapp/periphery" = "3.7.4" diff --git a/Sources/MistKit/Authentication/APITokenAuthenticator.swift b/Sources/MistKit/Authentication/APITokenAuthenticator.swift index 69cb0cea..9b7cff93 100644 --- a/Sources/MistKit/Authentication/APITokenAuthenticator.swift +++ b/Sources/MistKit/Authentication/APITokenAuthenticator.swift @@ -72,7 +72,7 @@ public struct APITokenAuthenticator: Authenticator { /// by `encoded()`. Re-runs format validation, so a corrupted or stale /// payload throws `TokenManagerError.invalidCredentials`. public init(decoding data: Data) throws { - let wire = try JSONDecoder().decode(WireFormat.self, from: data) + let wire = try JSONDecoder.shared.decode(WireFormat.self, from: data) try self.init(token: wire.token) } @@ -86,6 +86,6 @@ public struct APITokenAuthenticator: Authenticator { /// JSON-encodes the API token for persistence by `TokenStorage`. public func encoded() throws -> Data { - try JSONEncoder().encode(WireFormat(token: token)) + try JSONEncoder.shared.encode(WireFormat(token: token)) } } diff --git a/Sources/MistKit/Authentication/ServerToServerAuthenticator.swift b/Sources/MistKit/Authentication/ServerToServerAuthenticator.swift index a263fc5d..7bfa29ca 100644 --- a/Sources/MistKit/Authentication/ServerToServerAuthenticator.swift +++ b/Sources/MistKit/Authentication/ServerToServerAuthenticator.swift @@ -141,7 +141,7 @@ public struct ServerToServerAuthenticator: Authenticator { /// produced by `encoded()`. Re-runs key parse + key-ID validation, so a /// corrupted payload throws `TokenManagerError.invalidCredentials`. public init(decoding data: Data) throws { - let wire = try JSONDecoder().decode(WireFormat.self, from: data) + let wire = try JSONDecoder.shared.decode(WireFormat.self, from: data) guard let keyData = Data(base64Encoded: wire.privateKey) else { throw TokenManagerError.invalidCredentials(.encodedPayloadInvalidBase64) } @@ -191,6 +191,6 @@ public struct ServerToServerAuthenticator: Authenticator { privateKey: privateKey.rawRepresentation.base64EncodedString(), bodyBufferLimit: bodyBufferLimit ) - return try JSONEncoder().encode(wire) + return try JSONEncoder.shared.encode(wire) } } diff --git a/Sources/MistKit/Authentication/WebAuthTokenAuthenticator.swift b/Sources/MistKit/Authentication/WebAuthTokenAuthenticator.swift index 6f4135b7..552b1ccf 100644 --- a/Sources/MistKit/Authentication/WebAuthTokenAuthenticator.swift +++ b/Sources/MistKit/Authentication/WebAuthTokenAuthenticator.swift @@ -95,7 +95,7 @@ public struct WebAuthTokenAuthenticator: Authenticator { /// produced by `encoded()`. Re-runs format validation, so a corrupted /// or stale payload throws `TokenManagerError.invalidCredentials`. public init(decoding data: Data) throws { - let wire = try JSONDecoder().decode(WireFormat.self, from: data) + let wire = try JSONDecoder.shared.decode(WireFormat.self, from: data) try self.init(apiToken: wire.apiToken, webAuthToken: wire.webAuthToken) } @@ -114,6 +114,6 @@ public struct WebAuthTokenAuthenticator: Authenticator { /// JSON-encodes both tokens for persistence by `TokenStorage`. public func encoded() throws -> Data { - try JSONEncoder().encode(WireFormat(apiToken: apiToken, webAuthToken: webAuthToken)) + try JSONEncoder.shared.encode(WireFormat(apiToken: apiToken, webAuthToken: webAuthToken)) } } diff --git a/Sources/MistKit/CloudKitService/CloudKitError+OpenAPI.swift b/Sources/MistKit/CloudKitService/CloudKitError+OpenAPI.swift index b663404a..dcfbfb57 100644 --- a/Sources/MistKit/CloudKitService/CloudKitError+OpenAPI.swift +++ b/Sources/MistKit/CloudKitService/CloudKitError+OpenAPI.swift @@ -73,6 +73,19 @@ extension CloudKitError { } } + /// Build an `.httpError` for an undocumented response and log the occurrence. + /// The full response value is logged at `.debug` because it may echo server-side + /// request data (e.g. emails passed to `lookupUsersByEmail`); the `.warning` line + /// stays sanitized so it can ship to ops/log aggregators without leaking PII. + internal static func undocumented(statusCode: Int, response: some Any) -> CloudKitError { + let logger = Logger(subsystem: .api) + logger.debug("Unhandled response (HTTP \(statusCode)): \(response)") + logger.warning( + "Unhandled \(type(of: response)) (HTTP \(statusCode)) - treating as generic HTTP error" + ) + return .httpError(statusCode: statusCode) + } + /// Returns a copy of this error with the given hint attached. /// /// If `self` is already `.quotaExceeded`, the existing reason is preserved @@ -86,7 +99,9 @@ extension CloudKitError { /// with information that can only be computed from the local request state /// (e.g., the actual encoded record size, the asset byte count). internal func addingQuotaHint(_ hint: QuotaHint?) -> CloudKitError { - guard let hint else { return self } + guard let hint else { + return self + } switch self { case .quotaExceeded(let reason, _): return .quotaExceeded(reason: reason, hint: hint) @@ -98,17 +113,4 @@ extension CloudKitError { return self } } - - /// Build an `.httpError` for an undocumented response and log the occurrence. - /// The full response value is logged at `.debug` because it may echo server-side - /// request data (e.g. emails passed to `lookupUsersByEmail`); the `.warning` line - /// stays sanitized so it can ship to ops/log aggregators without leaking PII. - internal static func undocumented(statusCode: Int, response: some Any) -> CloudKitError { - let logger = Logger(subsystem: .api) - logger.debug("Unhandled response (HTTP \(statusCode)): \(response)") - logger.warning( - "Unhandled \(type(of: response)) (HTTP \(statusCode)) - treating as generic HTTP error" - ) - return .httpError(statusCode: statusCode) - } } diff --git a/Sources/MistKit/CloudKitService/CloudKitService+AssetUpload.swift b/Sources/MistKit/CloudKitService/CloudKitService+AssetUpload.swift index 6de368bf..d432fbbd 100644 --- a/Sources/MistKit/CloudKitService/CloudKitService+AssetUpload.swift +++ b/Sources/MistKit/CloudKitService/CloudKitService+AssetUpload.swift @@ -40,6 +40,20 @@ internal import Logging @available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *) extension CloudKitService { + /// Returns a `.assetExceedsSizeLimit` hint when local `data` is over + /// CloudKit's per-asset upload limit. Returns `nil` otherwise (the + /// `QUOTA_EXCEEDED` is presumably caused by the user's iCloud storage + /// being full, not the asset size). + private static func assetSizeQuotaHint(for data: Data) -> QuotaHint? { + guard data.count > maxAssetUploadBytes else { + return nil + } + return .assetExceedsSizeLimit( + dataBytes: data.count, + maxBytes: maxAssetUploadBytes + ) + } + /// Upload binary data to a CloudKit asset upload URL /// /// This is step 2 of the two-step asset upload process. @@ -84,7 +98,7 @@ extension CloudKitService { Logger(subsystem: .api).debug("Asset upload response: \(responseString)") } - let uploadResponse = try JSONDecoder().decode( + let uploadResponse = try JSONDecoder.shared.decode( AssetUploadResponse.self, from: responseData ) @@ -101,16 +115,4 @@ extension CloudKitService { .addingQuotaHint(Self.assetSizeQuotaHint(for: data)) } } - - /// Returns a `.assetExceedsSizeLimit` hint when local `data` is over - /// CloudKit's per-asset upload limit. Returns `nil` otherwise (the - /// `QUOTA_EXCEEDED` is presumably caused by the user's iCloud storage - /// being full, not the asset size). - private static func assetSizeQuotaHint(for data: Data) -> QuotaHint? { - guard data.count > maxAssetUploadBytes else { return nil } - return .assetExceedsSizeLimit( - dataBytes: data.count, - maxBytes: maxAssetUploadBytes - ) - } } diff --git a/Sources/MistKit/CloudKitService/CloudKitService+WriteOperations.swift b/Sources/MistKit/CloudKitService/CloudKitService+WriteOperations.swift index 0a82e207..c6b9322b 100644 --- a/Sources/MistKit/CloudKitService/CloudKitService+WriteOperations.swift +++ b/Sources/MistKit/CloudKitService/CloudKitService+WriteOperations.swift @@ -40,6 +40,28 @@ internal import OpenAPIRuntime #endif extension CloudKitService { + /// Inspect a batch of API record operations and return a `QuotaHint` for + /// the first record whose JSON-encoded size exceeds CloudKit's per-record + /// limit. Returns `nil` if every record is within bounds — which is the + /// usual case when the server's `QUOTA_EXCEEDED` is caused by storage-quota + /// exhaustion rather than per-record size. + private static func recordSizeQuotaHint( + for apiOperations: [Components.Schemas.RecordOperation] + ) -> QuotaHint? { + for (index, operation) in apiOperations.enumerated() { + guard let record = operation.record, + let encoded = try? JSONEncoder.shared.encode(record), + encoded.count > maxRecordDataBytes + else { continue } + return .recordExceedsSizeLimit( + operationIndex: index, + encodedBytes: encoded.count, + maxBytes: maxRecordDataBytes + ) + } + return nil + } + /// Modify (create, update, or delete) CloudKit records /// - Parameters: /// - operations: Array of record operations to perform @@ -96,29 +118,6 @@ extension CloudKitService { } } - /// Inspect a batch of API record operations and return a `QuotaHint` for - /// the first record whose JSON-encoded size exceeds CloudKit's per-record - /// limit. Returns `nil` if every record is within bounds — which is the - /// usual case when the server's `QUOTA_EXCEEDED` is caused by storage-quota - /// exhaustion rather than per-record size. - private static func recordSizeQuotaHint( - for apiOperations: [Components.Schemas.RecordOperation] - ) -> QuotaHint? { - let encoder = JSONEncoder() - for (index, operation) in apiOperations.enumerated() { - guard let record = operation.record, - let encoded = try? encoder.encode(record), - encoded.count > maxRecordDataBytes - else { continue } - return .recordExceedsSizeLimit( - operationIndex: index, - encodedBytes: encoded.count, - maxBytes: maxRecordDataBytes - ) - } - return nil - } - /// Create a single record in CloudKit /// - Parameters: /// - recordType: The type of record to create diff --git a/Sources/MistKit/CloudKitService/CloudKitService.swift b/Sources/MistKit/CloudKitService/CloudKitService.swift index 02b06e01..cdfcbddc 100644 --- a/Sources/MistKit/CloudKitService/CloudKitService.swift +++ b/Sources/MistKit/CloudKitService/CloudKitService.swift @@ -54,6 +54,7 @@ internal import OpenAPIRuntime /// `fetchCaller` via web-auth from one fully-populated `Credentials`. public struct CloudKitService: Sendable { // swiftlint:disable force_unwrapping + // swift-format-ignore: NeverForceUnwrap /// The base URL for CloudKit Web Services. public static let baseURL = URL(string: "https://api.apple-cloudkit.com")! // swiftlint:enable force_unwrapping diff --git a/Sources/MistKit/Extensions/JSONDecoder+Shared.swift b/Sources/MistKit/Extensions/JSONDecoder+Shared.swift new file mode 100644 index 00000000..0f707985 --- /dev/null +++ b/Sources/MistKit/Extensions/JSONDecoder+Shared.swift @@ -0,0 +1,37 @@ +// +// JSONDecoder+Shared.swift +// MistKit +// +// Created by Leo Dion. +// Copyright © 2026 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +internal import Foundation + +extension JSONDecoder { + /// Shared default-configured decoder. `JSONDecoder.decode(_:from:)` is + /// documented thread-safe once configuration is finalized, so a single + /// instance is safe across concurrent decoders. + internal static let shared = JSONDecoder() +} diff --git a/Sources/MistKit/Extensions/JSONEncoder+Shared.swift b/Sources/MistKit/Extensions/JSONEncoder+Shared.swift new file mode 100644 index 00000000..a5855a34 --- /dev/null +++ b/Sources/MistKit/Extensions/JSONEncoder+Shared.swift @@ -0,0 +1,37 @@ +// +// JSONEncoder+Shared.swift +// MistKit +// +// Created by Leo Dion. +// Copyright © 2026 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +internal import Foundation + +extension JSONEncoder { + /// Shared default-configured encoder. `JSONEncoder.encode(_:)` is documented + /// thread-safe once configuration is finalized, so a single instance is + /// safe across concurrent encoders. + internal static let shared = JSONEncoder() +} diff --git a/Sources/MistKit/Models/RecordOperation+EncodedSize.swift b/Sources/MistKit/Models/RecordOperation+EncodedSize.swift index ddd0b80d..a30a6414 100644 --- a/Sources/MistKit/Models/RecordOperation+EncodedSize.swift +++ b/Sources/MistKit/Models/RecordOperation+EncodedSize.swift @@ -45,7 +45,9 @@ extension RecordOperation { /// ``CloudKitService/maxAssetUploadBytes``. public func encodedRecordSize() throws -> Int { let apiOperation = try Components.Schemas.RecordOperation(from: self) - guard let record = apiOperation.record else { return 0 } - return try JSONEncoder().encode(record).count + guard let record = apiOperation.record else { + return 0 + } + return try JSONEncoder.shared.encode(record).count } } diff --git a/Tests/MistKitTests/CloudKitService/SizeLimits/CloudKitServiceTests.SizeLimits+Assets.swift b/Tests/MistKitTests/CloudKitService/SizeLimits/CloudKitServiceTests.SizeLimits+Assets.swift index a08de99e..2d320cea 100644 --- a/Tests/MistKitTests/CloudKitService/SizeLimits/CloudKitServiceTests.SizeLimits+Assets.swift +++ b/Tests/MistKitTests/CloudKitService/SizeLimits/CloudKitServiceTests.SizeLimits+Assets.swift @@ -99,7 +99,9 @@ extension CloudKitServiceTests.SizeLimits { using: Self.failingUploader(status: 413) ) } throws: { error in - guard let ckError = error as? CloudKitError else { return false } + guard let ckError = error as? CloudKitError else { + return false + } // No quota hint should be attached — the bare 413 propagates as-is. if case .quotaExceeded(_, let hint) = ckError, hint != nil { return false From 0759add3dd10130225abe7659343e7b61172891f Mon Sep 17 00:00:00 2001 From: Leo Dion Date: Tue, 19 May 2026 11:06:24 +0100 Subject: [PATCH 06/35] Fix subrepo parents after local rebase Co-Authored-By: Claude Opus 4.7 (1M context) --- Examples/BushelCloud/.gitrepo | 2 +- Examples/CelestraCloud/.gitrepo | 2 +- Packages/ConfigKeyKit/.gitrepo | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Examples/BushelCloud/.gitrepo b/Examples/BushelCloud/.gitrepo index 26600d51..cd66c7f0 100644 --- a/Examples/BushelCloud/.gitrepo +++ b/Examples/BushelCloud/.gitrepo @@ -7,6 +7,6 @@ remote = git@github.com:brightdigit/BushelCloud.git branch = mistkit commit = 66b595eb2e9d3a12a385edaae4a0e549f9d48da5 - parent = c31250a988eede3e8523ac6b97096ec2c91e99b2 + parent = abff797b5cea271cf680f60e3e6d34ea359925d9 method = merge cmdver = 0.4.9 diff --git a/Examples/CelestraCloud/.gitrepo b/Examples/CelestraCloud/.gitrepo index e16d2783..43366061 100644 --- a/Examples/CelestraCloud/.gitrepo +++ b/Examples/CelestraCloud/.gitrepo @@ -7,6 +7,6 @@ remote = git@github.com:brightdigit/CelestraCloud.git branch = mistkit commit = d91df88dcfe6b8c7cccd2d8257edb0472059ac2f - parent = 3e7a61518aaffa14c259c38087bf8ca75bf080cf + parent = abff797b5cea271cf680f60e3e6d34ea359925d9 method = merge cmdver = 0.4.9 diff --git a/Packages/ConfigKeyKit/.gitrepo b/Packages/ConfigKeyKit/.gitrepo index 0d4e3142..843af89e 100644 --- a/Packages/ConfigKeyKit/.gitrepo +++ b/Packages/ConfigKeyKit/.gitrepo @@ -7,6 +7,6 @@ remote = git@github.com:brightdigit/ConfigKeyKit.git branch = main commit = a9a8bc8be5b33d4aa732a9d0d06a05e8281b4855 - parent = 5d1a87aeaffbdfc883b2d13467503dda592d1ec0 + parent = abff797b5cea271cf680f60e3e6d34ea359925d9 method = merge cmdver = 0.4.9 From cf6bec2a6f0e84921c53eb4c74b7fc4a8e5dcea4 Mon Sep 17 00:00:00 2001 From: Leo Dion Date: Tue, 19 May 2026 13:44:42 +0100 Subject: [PATCH 07/35] Docs: sync authentication-middleware diagrams and prose with code MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Updates the internals doc to match the current source: - `RequestSignature` parameter is `webServiceSubpath`, not `webServiceURL` (S2S sequence diagram, signing-payload template, both initializer labels, and the embedded code example). - Token-Manager-Selection flowchart no longer labels the `APITokenAuthenticator` arrow as "user-attributed" — API-token-only is neither of the two CloudKit attribution modes defined by `PublicAuthPreference`. - Drop stale reference to `downgradeToAPIOnly()` / `updateWebAuthToken(_:)`; only `upgradeToWebAuthentication(_:)` exists. - Fix `Credentials/Credentials+TokenManager.swift` path — the file lives directly under `Authentication/`, not in a `Credentials/` subdirectory. Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/internals/authentication-middleware.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/internals/authentication-middleware.md b/docs/internals/authentication-middleware.md index 7354521b..b4994036 100644 --- a/docs/internals/authentication-middleware.md +++ b/docs/internals/authentication-middleware.md @@ -128,7 +128,7 @@ public func authenticate( keyID: keyID, privateKey: privateKey, requestBody: bodyData, - webServiceURL: request.path ?? "" + webServiceSubpath: request.path ) request.headerFields.append(contentsOf: signature.headers) @@ -156,15 +156,15 @@ It's a transport-format value, not a domain value: ### Signing process -The convenience initializer `init(keyID:privateKey:requestBody:webServiceURL:date:)` does: +The convenience initializer `init(keyID:privateKey:requestBody:webServiceSubpath:date:)` does: 1. **Format the ISO 8601 date.** On macOS 12 / iOS 15 / tvOS 15 / watchOS 8 and later, `Date.ISO8601FormatStyle` (Sendable value type). On older OSes, a `nonisolated(unsafe)` cached `ISO8601DateFormatter` (documented thread-safe for `string(from:)`). 2. **Hash the body.** `SHA256.cloudKitBodyHash(of: body)` returns `base64(SHA256(body))`, or the empty string when the body is `nil` — matching CloudKit's no-body convention. -3. **Build the signing payload:** `"::"` +3. **Build the signing payload:** `"::"` 4. **Sign with P-256.** `privateKey.signature(for: Data(payload.utf8))` → DER bytes. 5. **Delegate to the storage init**, capturing `iso8601DateString`, `signatureDerRepresentation`, and `keyID`. -A second initializer — `init(keyID:privateKey:bodyHash:webServiceURL:iso8601DateString:)` — takes the pre-formatted strings directly. It's the core signing path (no formatting, no hashing); the convenience init delegates to it. Useful for deterministic testing or when the caller already has those values. +A second initializer — `init(keyID:privateKey:bodyHash:webServiceSubpath:iso8601DateString:)` — takes the pre-formatted strings directly. It's the core signing path (no formatting, no hashing); the convenience init delegates to it. Useful for deterministic testing or when the caller already has those values. ### Wire format @@ -215,7 +215,7 @@ public actor AdaptiveTokenManager: TokenManager { } ``` -`WebAuthTokenAuthenticator`'s initializer is what validates the token (empty / too-short tokens throw `TokenManagerError.invalidCredentials`), so the manager doesn't duplicate that logic. The companion `downgradeToAPIOnly()` and `updateWebAuthToken(_:)` methods live alongside on `AdaptiveTokenManager+Transitions`. +`WebAuthTokenAuthenticator`'s initializer is what validates the token (empty / too-short tokens throw `TokenManagerError.invalidCredentials`), so the manager doesn't duplicate that logic. The `upgradeToWebAuthentication(_:)` method lives on `AdaptiveTokenManager+Transitions`. A typical client-app flow: @@ -245,7 +245,7 @@ public enum Database { There is **no default** — every public-database call picks explicitly. User-context routes (`/users/*`) pass `.public(.requires(.webAuth))` directly because CloudKit only accepts web-auth on those endpoints. Private and shared databases ignore this — they always require web-auth, since CloudKit rejects S2S on those scopes. -See `Sources/MistKit/Authentication/PublicAuthPreference.swift` and `Sources/MistKit/Authentication/Credentials/Credentials+TokenManager.swift` for the resolution logic. +See `Sources/MistKit/Authentication/PublicAuthPreference.swift` and `Sources/MistKit/Authentication/Credentials+TokenManager.swift` for the resolution logic. ## Complete Authentication Flow @@ -320,7 +320,7 @@ sequenceDiagram Mid->>Auth: authenticate(&request, &body) Auth->>Auth: buffer body and reassign as replayable HTTPBody - Auth->>Sig: RequestSignature(keyID, privateKey, requestBody, webServiceURL) + Auth->>Sig: RequestSignature(keyID, privateKey, requestBody, webServiceSubpath) Sig-->>Auth: signed header bundle Note over Auth: attach credentials to request Auth->>Auth: request.headerFields.append(contentsOf: signature.headers) @@ -343,7 +343,7 @@ flowchart LR Adapt -. "before upgrade" .-> APIA Adapt -. "after upgradeToWebAuthentication(_:)" .-> WAA - APIA --> Pub["Public DB (user-attributed)"] + APIA --> Pub["Public DB"] WAA --> PrivShared["Private / Shared DB"] S2SA --> PubS2S["Public DB (service-attributed)"] ``` From 99782cedd19ed709c5fc316c52205c1b6297ddc9 Mon Sep 17 00:00:00 2001 From: Leo Dion Date: Tue, 19 May 2026 14:50:40 +0100 Subject: [PATCH 08/35] Zone API: createZone, deleteZone, fetchAllZoneChanges (#367) Finishes the Zone API surface called out in #367 as "not shipped": single-zone create/delete convenience helpers over modifyZones, and the auto-paginating fetchAllZoneChanges (carve-out from #307). Library: - createZone / deleteZone (flat-param, mirrors createRecord pattern) - fetchAllZoneChanges with maxPages ceiling, cancellation check, stuck-token detection, and invalid-response guard - zonePaginationLimitExceeded sibling case on CloudKitError so the partial-results contract stays typed [ZoneInfo] rather than [RecordInfo] MistDemo: - create-zone / delete-zone CLI subcommands - ZoneRoundtripPhase + FetchAllZoneChangesPhase in PrivateDatabaseTest so the new endpoints are exercised against live CloudKit Closes #45, #47, #48 (audited as shipped per the parent issue), plus the zone-changes paginator carve-out from #307. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../Commands/CreateZoneCommand.swift | 105 +++++++++++++ .../Commands/DeleteZoneCommand.swift | 99 ++++++++++++ .../Configuration/CreateZoneConfig.swift | 101 ++++++++++++ .../Configuration/DeleteZoneConfig.swift | 101 ++++++++++++ .../Phases/FetchAllZoneChangesPhase.swift | 67 ++++++++ .../Phases/ZoneRoundtripPhase.swift | 71 +++++++++ .../Tests/PrivateDatabaseTest.swift | 2 + .../Sources/MistDemoKit/MistDemoRunner.swift | 2 + .../CloudKitService/CloudKitError.swift | 12 +- .../CloudKitService+FetchAllZoneChanges.swift | 114 ++++++++++++++ .../CloudKitService+ModifyZones.swift | 70 +++++++++ ...erviceTests.CreateZone+ErrorHandling.swift | 54 +++++++ ...udKitServiceTests.CreateZone+Helpers.swift | 94 +++++++++++ ...ServiceTests.CreateZone+SuccessCases.swift | 71 +++++++++ .../CloudKitServiceTests.CreateZone.swift | 38 +++++ ...udKitServiceTests.DeleteZone+Helpers.swift | 53 +++++++ ...ServiceTests.DeleteZone+SuccessCases.swift | 67 ++++++++ .../CloudKitServiceTests.DeleteZone.swift | 38 +++++ ...erviceTests.FetchZoneChanges+Helpers.swift | 8 +- ...hZoneChanges.SuccessCases+Pagination.swift | 148 ++++++++++++++++++ 20 files changed, 1312 insertions(+), 3 deletions(-) create mode 100644 Examples/MistDemo/Sources/MistDemoKit/Commands/CreateZoneCommand.swift create mode 100644 Examples/MistDemo/Sources/MistDemoKit/Commands/DeleteZoneCommand.swift create mode 100644 Examples/MistDemo/Sources/MistDemoKit/Configuration/CreateZoneConfig.swift create mode 100644 Examples/MistDemo/Sources/MistDemoKit/Configuration/DeleteZoneConfig.swift create mode 100644 Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/FetchAllZoneChangesPhase.swift create mode 100644 Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/ZoneRoundtripPhase.swift create mode 100644 Sources/MistKit/CloudKitService/CloudKitService+FetchAllZoneChanges.swift create mode 100644 Tests/MistKitTests/CloudKitService/CreateZone/CloudKitServiceTests.CreateZone+ErrorHandling.swift create mode 100644 Tests/MistKitTests/CloudKitService/CreateZone/CloudKitServiceTests.CreateZone+Helpers.swift create mode 100644 Tests/MistKitTests/CloudKitService/CreateZone/CloudKitServiceTests.CreateZone+SuccessCases.swift create mode 100644 Tests/MistKitTests/CloudKitService/CreateZone/CloudKitServiceTests.CreateZone.swift create mode 100644 Tests/MistKitTests/CloudKitService/DeleteZone/CloudKitServiceTests.DeleteZone+Helpers.swift create mode 100644 Tests/MistKitTests/CloudKitService/DeleteZone/CloudKitServiceTests.DeleteZone+SuccessCases.swift create mode 100644 Tests/MistKitTests/CloudKitService/DeleteZone/CloudKitServiceTests.DeleteZone.swift create mode 100644 Tests/MistKitTests/CloudKitService/FetchZoneChanges/CloudKitServiceTests.FetchZoneChanges.SuccessCases+Pagination.swift diff --git a/Examples/MistDemo/Sources/MistDemoKit/Commands/CreateZoneCommand.swift b/Examples/MistDemo/Sources/MistDemoKit/Commands/CreateZoneCommand.swift new file mode 100644 index 00000000..101692f9 --- /dev/null +++ b/Examples/MistDemo/Sources/MistDemoKit/Commands/CreateZoneCommand.swift @@ -0,0 +1,105 @@ +// +// CreateZoneCommand.swift +// MistDemo +// +// Created by Leo Dion. +// Copyright © 2026 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +internal import Foundation +internal import MistKit + +/// Command to create a single CloudKit zone. +public struct CreateZoneCommand: MistDemoCommand, OutputFormatting { + /// The configuration type. + public typealias Config = CreateZoneConfig + /// The command name. + public static let commandName = "create-zone" + /// The command abstract. + public static let abstract = "Create a single CloudKit zone" + /// The command help text. + public static let helpText = """ + CREATE-ZONE - Create a single CloudKit zone + + USAGE: + mistdemo create-zone --zone-name [options] + + OPTIONS: + --zone-name Zone name to create (required) + --zone-owner Optional owner record name + --database Database to target (private or shared) + --output-format Output format + + EXAMPLES: + mistdemo create-zone --zone-name Articles + mistdemo create-zone --zone-name SharedZone --database shared + + NOTES: + - The .public database does not support custom zones + - Auth method follows --database + - Zone names are case-sensitive + """ + + private let config: CreateZoneConfig + + /// Creates a new instance. + public init(config: CreateZoneConfig) { + self.config = config + } + + /// Executes the command. + public func execute() async throws { + print("\n" + String(repeating: "=", count: 60)) + print("🆕 Create CloudKit Zone") + print(String(repeating: "=", count: 60)) + + let service = try MistKitClientFactory.create(for: config.base) + + print("\n📋 Creating zone:") + print(" - Name: \(config.zoneName)") + if let owner = config.ownerRecordName { + print(" - Owner: \(owner)") + } + print(" - Database: \(config.base.database)") + + let zone = try await service.createZone( + zoneName: config.zoneName, + ownerRecordName: config.ownerRecordName, + database: config.base.database + ) + + print("\n✅ Created zone:") + print(" - \(zone.zoneName)") + if let owner = zone.ownerRecordName { + print(" Owner: \(owner)") + } + if !zone.capabilities.isEmpty { + print(" Capabilities: \(zone.capabilities.joined(separator: ", "))") + } + + print("\n" + String(repeating: "=", count: 60)) + print("✅ Zone creation completed!") + print(String(repeating: "=", count: 60)) + } +} diff --git a/Examples/MistDemo/Sources/MistDemoKit/Commands/DeleteZoneCommand.swift b/Examples/MistDemo/Sources/MistDemoKit/Commands/DeleteZoneCommand.swift new file mode 100644 index 00000000..a6be8d05 --- /dev/null +++ b/Examples/MistDemo/Sources/MistDemoKit/Commands/DeleteZoneCommand.swift @@ -0,0 +1,99 @@ +// +// DeleteZoneCommand.swift +// MistDemo +// +// Created by Leo Dion. +// Copyright © 2026 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +internal import Foundation +internal import MistKit + +/// Command to delete a single CloudKit zone. +public struct DeleteZoneCommand: MistDemoCommand, OutputFormatting { + /// The configuration type. + public typealias Config = DeleteZoneConfig + /// The command name. + public static let commandName = "delete-zone" + /// The command abstract. + public static let abstract = "Delete a single CloudKit zone" + /// The command help text. + public static let helpText = """ + DELETE-ZONE - Delete a single CloudKit zone + + USAGE: + mistdemo delete-zone --zone-name [options] + + OPTIONS: + --zone-name Zone name to delete (required) + --zone-owner Optional owner record name + --database Database to target (private or shared) + --output-format Output format + + EXAMPLES: + mistdemo delete-zone --zone-name Articles + mistdemo delete-zone --zone-name SharedZone --database shared + + NOTES: + - The .public database does not support custom zones + - Deleting a zone removes ALL records inside it + - Auth method follows --database + - Zone names are case-sensitive + """ + + private let config: DeleteZoneConfig + + /// Creates a new instance. + public init(config: DeleteZoneConfig) { + self.config = config + } + + /// Executes the command. + public func execute() async throws { + print("\n" + String(repeating: "=", count: 60)) + print("🗑️ Delete CloudKit Zone") + print(String(repeating: "=", count: 60)) + + let service = try MistKitClientFactory.create(for: config.base) + + print("\n📋 Deleting zone:") + print(" - Name: \(config.zoneName)") + if let owner = config.ownerRecordName { + print(" - Owner: \(owner)") + } + print(" - Database: \(config.base.database)") + + try await service.deleteZone( + zoneName: config.zoneName, + ownerRecordName: config.ownerRecordName, + database: config.base.database + ) + + print("\n✅ Deleted zone '\(config.zoneName)'") + + print("\n" + String(repeating: "=", count: 60)) + print("✅ Zone deletion completed!") + print(String(repeating: "=", count: 60)) + } +} diff --git a/Examples/MistDemo/Sources/MistDemoKit/Configuration/CreateZoneConfig.swift b/Examples/MistDemo/Sources/MistDemoKit/Configuration/CreateZoneConfig.swift new file mode 100644 index 00000000..d00010eb --- /dev/null +++ b/Examples/MistDemo/Sources/MistDemoKit/Configuration/CreateZoneConfig.swift @@ -0,0 +1,101 @@ +// +// CreateZoneConfig.swift +// MistDemo +// +// Created by Leo Dion. +// Copyright © 2026 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +public import ConfigKeyKit +internal import Foundation + +/// Configuration for create-zone command. +public struct CreateZoneConfig: Sendable, ConfigurationParseable { + /// The configuration reader type. + public typealias ConfigReader = MistDemoConfiguration + /// The base configuration type. + public typealias BaseConfig = MistDemoConfig + + /// The base MistDemo configuration. + public let base: MistDemoConfig + /// The zone name to create. + public let zoneName: String + /// Optional owner record name (typically nil for the caller's own zones). + public let ownerRecordName: String? + /// The output format. + public let output: OutputFormat + + /// Creates a new instance. + public init( + base: MistDemoConfig, + zoneName: String, + ownerRecordName: String? = nil, + output: OutputFormat = .table + ) { + self.base = base + self.zoneName = zoneName + self.ownerRecordName = ownerRecordName + self.output = output + } + + /// Parse configuration from command line arguments. + public init( + configuration: MistDemoConfiguration, + base: MistDemoConfig? + ) async throws { + let baseConfig: MistDemoConfig + if let base { + baseConfig = base + } else { + baseConfig = try await MistDemoConfig( + configuration: configuration, + base: nil + ) + } + + guard + let zoneName = configuration.string(forKey: "zone.name"), + !zoneName.isEmpty + else { + throw ConfigurationError.missingRequired( + "zone.name", + suggestion: "Pass --zone-name ." + ) + } + + let ownerRecordName = configuration.string(forKey: "zone.owner") + + let outputString = + configuration.string(forKey: "output.format", default: "table") + ?? "table" + let output = OutputFormat(rawValue: outputString) ?? .table + + self.init( + base: baseConfig, + zoneName: zoneName, + ownerRecordName: ownerRecordName, + output: output + ) + } +} diff --git a/Examples/MistDemo/Sources/MistDemoKit/Configuration/DeleteZoneConfig.swift b/Examples/MistDemo/Sources/MistDemoKit/Configuration/DeleteZoneConfig.swift new file mode 100644 index 00000000..d98988eb --- /dev/null +++ b/Examples/MistDemo/Sources/MistDemoKit/Configuration/DeleteZoneConfig.swift @@ -0,0 +1,101 @@ +// +// DeleteZoneConfig.swift +// MistDemo +// +// Created by Leo Dion. +// Copyright © 2026 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +public import ConfigKeyKit +internal import Foundation + +/// Configuration for delete-zone command. +public struct DeleteZoneConfig: Sendable, ConfigurationParseable { + /// The configuration reader type. + public typealias ConfigReader = MistDemoConfiguration + /// The base configuration type. + public typealias BaseConfig = MistDemoConfig + + /// The base MistDemo configuration. + public let base: MistDemoConfig + /// The zone name to delete. + public let zoneName: String + /// Optional owner record name (typically nil for the caller's own zones). + public let ownerRecordName: String? + /// The output format. + public let output: OutputFormat + + /// Creates a new instance. + public init( + base: MistDemoConfig, + zoneName: String, + ownerRecordName: String? = nil, + output: OutputFormat = .table + ) { + self.base = base + self.zoneName = zoneName + self.ownerRecordName = ownerRecordName + self.output = output + } + + /// Parse configuration from command line arguments. + public init( + configuration: MistDemoConfiguration, + base: MistDemoConfig? + ) async throws { + let baseConfig: MistDemoConfig + if let base { + baseConfig = base + } else { + baseConfig = try await MistDemoConfig( + configuration: configuration, + base: nil + ) + } + + guard + let zoneName = configuration.string(forKey: "zone.name"), + !zoneName.isEmpty + else { + throw ConfigurationError.missingRequired( + "zone.name", + suggestion: "Pass --zone-name ." + ) + } + + let ownerRecordName = configuration.string(forKey: "zone.owner") + + let outputString = + configuration.string(forKey: "output.format", default: "table") + ?? "table" + let output = OutputFormat(rawValue: outputString) ?? .table + + self.init( + base: baseConfig, + zoneName: zoneName, + ownerRecordName: ownerRecordName, + output: output + ) + } +} diff --git a/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/FetchAllZoneChangesPhase.swift b/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/FetchAllZoneChangesPhase.swift new file mode 100644 index 00000000..67bd7581 --- /dev/null +++ b/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/FetchAllZoneChangesPhase.swift @@ -0,0 +1,67 @@ +// +// FetchAllZoneChangesPhase.swift +// MistDemo +// +// Created by Leo Dion. +// Copyright © 2026 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +internal import Foundation +internal import MistKit + +/// Exercises ``CloudKitService/fetchAllZoneChanges(syncToken:maxPages:database:)`` +/// against a live container. Failures are non-fatal (matching +/// ``FetchZoneChangesPhase``) so test pipelines with empty zone change feeds +/// don't fail the whole suite. +internal struct FetchAllZoneChangesPhase: IntegrationPhase { + internal typealias Input = NoState + internal typealias Output = NoState + + internal static let title = "Fetch all zone changes" + internal static let emoji = "🔁" + internal static let apiName = "fetchAllZoneChanges" + + internal func run(input: NoState, context: PhaseContext) async throws -> NoState { + print("\n\(Self.emoji) \(Self.title)") + + do { + let (zones, token) = try await context.service.fetchAllZoneChanges( + database: context.database + ) + print("✅ Fetched \(zones.count) zone(s) across all pages") + if context.verbose { + for zone in zones { + print(" - \(zone.zoneName)") + } + if let token { + print(" Sync token: \(token.prefix(30))...") + } + } + } catch { + print("⚠️ fetchAllZoneChanges failed (non-fatal): \(error)") + } + + return NoState() + } +} diff --git a/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/ZoneRoundtripPhase.swift b/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/ZoneRoundtripPhase.swift new file mode 100644 index 00000000..b068714d --- /dev/null +++ b/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/ZoneRoundtripPhase.swift @@ -0,0 +1,71 @@ +// +// ZoneRoundtripPhase.swift +// MistDemo +// +// Created by Leo Dion. +// Copyright © 2026 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +internal import Foundation +internal import MistKit + +/// Create and immediately delete a uniquely-named zone, exercising both +/// ``CloudKitService/createZone(zoneName:ownerRecordName:database:)`` and +/// ``CloudKitService/deleteZone(zoneName:ownerRecordName:database:)`` in +/// a single self-cleaning phase. CloudKit only allows custom zones on the +/// private and shared databases. +internal struct ZoneRoundtripPhase: IntegrationPhase { + internal typealias Input = NoState + internal typealias Output = NoState + + internal static let title = "Create and delete a zone" + internal static let emoji = "🌀" + internal static let apiName = "createZone+deleteZone" + + internal func run(input: NoState, context: PhaseContext) async throws -> NoState { + print("\n\(Self.emoji) \(Self.title)") + + let zoneName = "mistkit-itest-\(UUID().uuidString.lowercased())" + + let created = try await context.service.createZone( + zoneName: zoneName, + database: context.database + ) + if context.verbose { + print(" ✅ Created zone: \(created.zoneName)") + } + + try await context.service.deleteZone( + zoneName: zoneName, + database: context.database + ) + if context.verbose { + print(" ✅ Deleted zone: \(zoneName)") + } + + print("✅ Roundtrip succeeded for zone '\(zoneName)'") + + return NoState() + } +} diff --git a/Examples/MistDemo/Sources/MistDemoKit/Integration/Tests/PrivateDatabaseTest.swift b/Examples/MistDemo/Sources/MistDemoKit/Integration/Tests/PrivateDatabaseTest.swift index 39d27c23..c754b6e7 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Integration/Tests/PrivateDatabaseTest.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Integration/Tests/PrivateDatabaseTest.swift @@ -42,7 +42,9 @@ internal struct PrivateDatabaseTest: PhasedIntegrationTest { internal let phases: [any IntegrationPhase] = [ ListZonesPhase(), LookupZonePhase(), + ZoneRoundtripPhase(), FetchZoneChangesPhase(), + FetchAllZoneChangesPhase(), UploadAssetPhase(), CreateRecordsPhase(), QueryRecordsPhase(), diff --git a/Examples/MistDemo/Sources/MistDemoKit/MistDemoRunner.swift b/Examples/MistDemo/Sources/MistDemoKit/MistDemoRunner.swift index 170f2f96..9df96a48 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/MistDemoRunner.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/MistDemoRunner.swift @@ -54,6 +54,8 @@ public enum MistDemoRunner { await registry.register(UploadAssetCommand.self) await registry.register(DemoInFilterCommand.self) await registry.register(LookupZonesCommand.self) + await registry.register(CreateZoneCommand.self) + await registry.register(DeleteZoneCommand.self) await registry.register(FetchChangesCommand.self) await registry.register(TestPublicCommand.self) await registry.register(TestPrivateCommand.self) diff --git a/Sources/MistKit/CloudKitService/CloudKitError.swift b/Sources/MistKit/CloudKitService/CloudKitError.swift index 4b4bcf25..e13578e9 100644 --- a/Sources/MistKit/CloudKitService/CloudKitError.swift +++ b/Sources/MistKit/CloudKitService/CloudKitError.swift @@ -58,6 +58,10 @@ public enum CloudKitError: LocalizedError, Sendable { case networkError(URLError) case unsupportedOperationType(String) case paginationLimitExceeded(maxPages: Int, records: [RecordInfo]) + /// Auto-paginating zone-changes call hit its `maxPages` ceiling. The + /// `zones` payload carries every zone collected before the cap was hit so + /// callers can resume from the partial result. + case zonePaginationLimitExceeded(maxPages: Int, zones: [ZoneInfo]) case missingCredentials( database: Database, availability: CredentialAvailability = .notConfigured, @@ -77,8 +81,8 @@ public enum CloudKitError: LocalizedError, Sendable { case .badRequest, .atomicFailure: return 400 case .invalidResponse, .underlyingError, .decodingError, .networkError, - .unsupportedOperationType, .paginationLimitExceeded, .missingCredentials, - .invalidPrivateKey: + .unsupportedOperationType, .paginationLimitExceeded, + .zonePaginationLimitExceeded, .missingCredentials, .invalidPrivateKey: return nil } } @@ -148,6 +152,10 @@ public enum CloudKitError: LocalizedError, Sendable { return "CloudKit query exceeded pagination limit of \(maxPages) pages " + "(collected \(records.count) records)" + case .zonePaginationLimitExceeded(let maxPages, let zones): + return + "CloudKit zone-changes exceeded pagination limit of \(maxPages) pages " + + "(collected \(zones.count) zones)" case .missingCredentials(let database, let availability, let reason): let availabilityLabel: String switch availability { diff --git a/Sources/MistKit/CloudKitService/CloudKitService+FetchAllZoneChanges.swift b/Sources/MistKit/CloudKitService/CloudKitService+FetchAllZoneChanges.swift new file mode 100644 index 00000000..4676050f --- /dev/null +++ b/Sources/MistKit/CloudKitService/CloudKitService+FetchAllZoneChanges.swift @@ -0,0 +1,114 @@ +// +// CloudKitService+FetchAllZoneChanges.swift +// MistKit +// +// Created by Leo Dion. +// Copyright © 2026 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +internal import Foundation + +extension CloudKitService { + /// Fetch all zone changes, handling pagination automatically. + /// + /// Convenience method that automatically fetches all available zone changes + /// by following the `moreComing` flag and making multiple requests if needed. + /// + /// - Parameters: + /// - syncToken: Optional token from previous fetch (nil = initial fetch) + /// - maxPages: Maximum number of pages to fetch before throwing + /// ``CloudKitError/zonePaginationLimitExceeded(maxPages:zones:)`` + /// (defaults to 1,000) + /// - database: The CloudKit database scope to query (defaults to `.private`) + /// - Returns: Array of all changed zones and final sync token. + /// - Throws: `CloudKitError`. When `maxPages` is exceeded, throws + /// ``CloudKitError/zonePaginationLimitExceeded(maxPages:zones:)`` whose + /// `zones` payload contains every zone collected before the cap was hit. + /// + /// Example: + /// ```swift + /// let (zones, newToken) = try await service.fetchAllZoneChanges( + /// syncToken: lastSyncToken + /// ) + /// processZones(zones) + /// // Store newToken for next sync + /// ``` + /// + /// - Warning: For databases with many zone changes, this may make multiple + /// requests and return a large array. Consider using + /// ``fetchZoneChanges(syncToken:database:)`` with manual pagination for + /// better memory control. + /// - Warning: This method will stop early if the server repeatedly returns + /// `moreComing: true` with no zones and the same sync token + /// (stuck-token scenario). + /// - Note: Makes sequential requests with no backoff or cooperative + /// cancellation between pages. For fine-grained control, use + /// ``fetchZoneChanges(syncToken:database:)`` directly. + public func fetchAllZoneChanges( + syncToken: String? = nil, + maxPages: Int = 1_000, + database: Database = .private + ) async throws(CloudKitError) -> (zones: [ZoneInfo], syncToken: String?) { + var allZones: [ZoneInfo] = [] + var currentToken = syncToken + var moreComing = false + var pageCount = 0 + + repeat { + guard pageCount < maxPages else { + throw CloudKitError.zonePaginationLimitExceeded( + maxPages: maxPages, + zones: allZones + ) + } + + do { + try Task.checkCancellation() + } catch { + throw mapToCloudKitError(error, context: "fetchAllZoneChanges") + } + + let result = try await fetchZoneChanges( + syncToken: currentToken, + database: database + ) + + // Stuck-token detection + if result.zones.isEmpty && result.moreComing && result.syncToken == currentToken { + break + } + + if result.moreComing && result.syncToken == nil { + throw CloudKitError.invalidResponse + } + + allZones.append(contentsOf: result.zones) + currentToken = result.syncToken + moreComing = result.moreComing + pageCount += 1 + } while moreComing + + return (allZones, currentToken) + } +} diff --git a/Sources/MistKit/CloudKitService/CloudKitService+ModifyZones.swift b/Sources/MistKit/CloudKitService/CloudKitService+ModifyZones.swift index 73ef3a89..11dd57a5 100644 --- a/Sources/MistKit/CloudKitService/CloudKitService+ModifyZones.swift +++ b/Sources/MistKit/CloudKitService/CloudKitService+ModifyZones.swift @@ -101,4 +101,74 @@ extension CloudKitService { throw mapToCloudKitError(error, context: "modifyZones") } } + + /// Create a single zone in the target database. + /// + /// Convenience wrapper over ``modifyZones(_:database:)`` for the common case + /// of creating one zone. CloudKit's `zones/modify` endpoint is only supported + /// on `.private` and `.shared`. + /// + /// - Parameters: + /// - zoneName: Non-empty zone name. Case-sensitive. + /// - ownerRecordName: Optional owner record name. Pass `nil` for the + /// caller's own zones (typical). + /// - database: Target database. Must not be `.public`. + /// - Returns: `ZoneInfo` for the created zone. + /// - Throws: `CloudKitError`. ``CloudKitError/invalidResponse`` if the + /// server returns no zone in its response. + /// + /// # Example + /// ```swift + /// let zone = try await service.createZone( + /// zoneName: "Articles", + /// database: .private + /// ) + /// ``` + public func createZone( + zoneName: String, + ownerRecordName: String? = nil, + database: Database + ) async throws(CloudKitError) -> ZoneInfo { + let operation = ZoneOperation.create( + ZoneID(zoneName: zoneName, ownerName: ownerRecordName) + ) + + let results = try await modifyZones([operation], database: database) + guard let zone = results.first else { + throw CloudKitError.invalidResponse + } + return zone + } + + /// Delete a single zone from the target database. + /// + /// Convenience wrapper over ``modifyZones(_:database:)`` for the common case + /// of deleting one zone. CloudKit's `zones/modify` endpoint is only supported + /// on `.private` and `.shared`. + /// + /// - Parameters: + /// - zoneName: Non-empty zone name. Case-sensitive. + /// - ownerRecordName: Optional owner record name. Pass `nil` for the + /// caller's own zones (typical). + /// - database: Target database. Must not be `.public`. + /// - Throws: `CloudKitError` if validation fails or the request fails. + /// + /// # Example + /// ```swift + /// try await service.deleteZone( + /// zoneName: "Articles", + /// database: .private + /// ) + /// ``` + public func deleteZone( + zoneName: String, + ownerRecordName: String? = nil, + database: Database + ) async throws(CloudKitError) { + let operation = ZoneOperation.delete( + ZoneID(zoneName: zoneName, ownerName: ownerRecordName) + ) + + _ = try await modifyZones([operation], database: database) + } } diff --git a/Tests/MistKitTests/CloudKitService/CreateZone/CloudKitServiceTests.CreateZone+ErrorHandling.swift b/Tests/MistKitTests/CloudKitService/CreateZone/CloudKitServiceTests.CreateZone+ErrorHandling.swift new file mode 100644 index 00000000..d1ecfa76 --- /dev/null +++ b/Tests/MistKitTests/CloudKitService/CreateZone/CloudKitServiceTests.CreateZone+ErrorHandling.swift @@ -0,0 +1,54 @@ +// +// CloudKitServiceTests.CreateZone+ErrorHandling.swift +// MistKit +// +// Created by Leo Dion. +// Copyright © 2026 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +internal import Foundation +internal import Testing + +@testable import MistKit + +extension CloudKitServiceTests.CreateZone { + @Suite("Error Handling") + internal struct ErrorHandling { + @Test("createZone() throws .invalidResponse when server returns no zones") + internal func createZoneEmptyResponseThrows() async throws { + guard #available(macOS 11.0, iOS 14.0, tvOS 14.0, watchOS 7.0, *) else { + Issue.record("CloudKitService is not available on this operating system.") + return + } + let service = try await CloudKitServiceTests.CreateZone.makeEmptyResponseService() + + await #expect(throws: CloudKitError.self) { + _ = try await service.createZone( + zoneName: "Articles", + database: .private + ) + } + } + } +} diff --git a/Tests/MistKitTests/CloudKitService/CreateZone/CloudKitServiceTests.CreateZone+Helpers.swift b/Tests/MistKitTests/CloudKitService/CreateZone/CloudKitServiceTests.CreateZone+Helpers.swift new file mode 100644 index 00000000..116672d1 --- /dev/null +++ b/Tests/MistKitTests/CloudKitService/CreateZone/CloudKitServiceTests.CreateZone+Helpers.swift @@ -0,0 +1,94 @@ +// +// CloudKitServiceTests.CreateZone+Helpers.swift +// MistKit +// +// Created by Leo Dion. +// Copyright © 2026 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +internal import Foundation +internal import HTTPTypes +internal import Testing + +@testable import MistKit + +extension CloudKitServiceTests.CreateZone { + private static let testAPIToken = TestConstants.apiToken + + internal static func makeSuccessfulService( + zoneCount: Int = 1 + ) async throws -> CloudKitService { + let responseProvider = try ResponseProvider.successfulModifyZones(zoneCount: zoneCount) + let transport = MockTransport(responseProvider: responseProvider) + return try CloudKitService( + containerIdentifier: TestConstants.serviceContainerIdentifier, + credentials: Credentials( + apiAuth: APICredentials( + apiToken: testAPIToken, + webAuthToken: TestConstants.webAuthToken + ) + ), + transport: transport + ) + } + + internal static func makeEmptyResponseService() async throws -> CloudKitService { + let responseProvider = ResponseProvider( + defaultResponse: try .emptyModifyZonesResponse() + ) + let transport = MockTransport(responseProvider: responseProvider) + return try CloudKitService( + containerIdentifier: TestConstants.serviceContainerIdentifier, + credentials: Credentials( + apiAuth: APICredentials( + apiToken: testAPIToken, + webAuthToken: TestConstants.webAuthToken + ) + ), + transport: transport + ) + } +} + +// MARK: - Empty Zones Response Builder + +extension ResponseConfig { + internal static func emptyModifyZonesResponse() throws -> ResponseConfig { + let responseJSON = """ + { + "zones": [] + } + """ + + var headers = HTTPFields() + headers[.contentType] = "application/json" + + return ResponseConfig( + statusCode: 200, + headers: headers, + body: responseJSON.data(using: .utf8), + error: nil + ) + } +} diff --git a/Tests/MistKitTests/CloudKitService/CreateZone/CloudKitServiceTests.CreateZone+SuccessCases.swift b/Tests/MistKitTests/CloudKitService/CreateZone/CloudKitServiceTests.CreateZone+SuccessCases.swift new file mode 100644 index 00000000..ce37d679 --- /dev/null +++ b/Tests/MistKitTests/CloudKitService/CreateZone/CloudKitServiceTests.CreateZone+SuccessCases.swift @@ -0,0 +1,71 @@ +// +// CloudKitServiceTests.CreateZone+SuccessCases.swift +// MistKit +// +// Created by Leo Dion. +// Copyright © 2026 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +internal import Foundation +internal import Testing + +@testable import MistKit + +extension CloudKitServiceTests.CreateZone { + @Suite("Success Cases") + internal struct SuccessCases { + @Test("createZone() returns ZoneInfo for a basic create") + internal func createZoneBasic() async throws { + guard #available(macOS 11.0, iOS 14.0, tvOS 14.0, watchOS 7.0, *) else { + Issue.record("CloudKitService is not available on this operating system.") + return + } + let service = try await CloudKitServiceTests.CreateZone.makeSuccessfulService(zoneCount: 1) + + let zone = try await service.createZone( + zoneName: "Articles", + database: .private + ) + + #expect(zone.zoneName == "modified-zone-0") + } + + @Test("createZone() forwards ownerRecordName to the operation") + internal func createZoneWithOwner() async throws { + guard #available(macOS 11.0, iOS 14.0, tvOS 14.0, watchOS 7.0, *) else { + Issue.record("CloudKitService is not available on this operating system.") + return + } + let service = try await CloudKitServiceTests.CreateZone.makeSuccessfulService(zoneCount: 1) + + let zone = try await service.createZone( + zoneName: "SharedZone", + ownerRecordName: "other-user", + database: .shared + ) + + #expect(zone.zoneName == "modified-zone-0") + } + } +} diff --git a/Tests/MistKitTests/CloudKitService/CreateZone/CloudKitServiceTests.CreateZone.swift b/Tests/MistKitTests/CloudKitService/CreateZone/CloudKitServiceTests.CreateZone.swift new file mode 100644 index 00000000..315a58fd --- /dev/null +++ b/Tests/MistKitTests/CloudKitService/CreateZone/CloudKitServiceTests.CreateZone.swift @@ -0,0 +1,38 @@ +// +// CloudKitServiceTests.CreateZone.swift +// MistKit +// +// Created by Leo Dion. +// Copyright © 2026 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +internal import Foundation +internal import Testing + +@testable import MistKit + +extension CloudKitServiceTests { + @Suite("CloudKitService CreateZone Operations", .enabled(if: Platform.isCryptoAvailable)) + internal enum CreateZone {} +} diff --git a/Tests/MistKitTests/CloudKitService/DeleteZone/CloudKitServiceTests.DeleteZone+Helpers.swift b/Tests/MistKitTests/CloudKitService/DeleteZone/CloudKitServiceTests.DeleteZone+Helpers.swift new file mode 100644 index 00000000..3b75399f --- /dev/null +++ b/Tests/MistKitTests/CloudKitService/DeleteZone/CloudKitServiceTests.DeleteZone+Helpers.swift @@ -0,0 +1,53 @@ +// +// CloudKitServiceTests.DeleteZone+Helpers.swift +// MistKit +// +// Created by Leo Dion. +// Copyright © 2026 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +internal import Foundation +internal import HTTPTypes +internal import Testing + +@testable import MistKit + +extension CloudKitServiceTests.DeleteZone { + private static let testAPIToken = TestConstants.apiToken + + internal static func makeSuccessfulService() async throws -> CloudKitService { + let responseProvider = try ResponseProvider.successfulModifyZones(zoneCount: 1) + let transport = MockTransport(responseProvider: responseProvider) + return try CloudKitService( + containerIdentifier: TestConstants.serviceContainerIdentifier, + credentials: Credentials( + apiAuth: APICredentials( + apiToken: testAPIToken, + webAuthToken: TestConstants.webAuthToken + ) + ), + transport: transport + ) + } +} diff --git a/Tests/MistKitTests/CloudKitService/DeleteZone/CloudKitServiceTests.DeleteZone+SuccessCases.swift b/Tests/MistKitTests/CloudKitService/DeleteZone/CloudKitServiceTests.DeleteZone+SuccessCases.swift new file mode 100644 index 00000000..baee18d7 --- /dev/null +++ b/Tests/MistKitTests/CloudKitService/DeleteZone/CloudKitServiceTests.DeleteZone+SuccessCases.swift @@ -0,0 +1,67 @@ +// +// CloudKitServiceTests.DeleteZone+SuccessCases.swift +// MistKit +// +// Created by Leo Dion. +// Copyright © 2026 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +internal import Foundation +internal import Testing + +@testable import MistKit + +extension CloudKitServiceTests.DeleteZone { + @Suite("Success Cases") + internal struct SuccessCases { + @Test("deleteZone() completes successfully") + internal func deleteZoneBasic() async throws { + guard #available(macOS 11.0, iOS 14.0, tvOS 14.0, watchOS 7.0, *) else { + Issue.record("CloudKitService is not available on this operating system.") + return + } + let service = try await CloudKitServiceTests.DeleteZone.makeSuccessfulService() + + try await service.deleteZone( + zoneName: "Archive", + database: .private + ) + } + + @Test("deleteZone() accepts ownerRecordName parameter") + internal func deleteZoneWithOwner() async throws { + guard #available(macOS 11.0, iOS 14.0, tvOS 14.0, watchOS 7.0, *) else { + Issue.record("CloudKitService is not available on this operating system.") + return + } + let service = try await CloudKitServiceTests.DeleteZone.makeSuccessfulService() + + try await service.deleteZone( + zoneName: "SharedZone", + ownerRecordName: "other-user", + database: .shared + ) + } + } +} diff --git a/Tests/MistKitTests/CloudKitService/DeleteZone/CloudKitServiceTests.DeleteZone.swift b/Tests/MistKitTests/CloudKitService/DeleteZone/CloudKitServiceTests.DeleteZone.swift new file mode 100644 index 00000000..57256381 --- /dev/null +++ b/Tests/MistKitTests/CloudKitService/DeleteZone/CloudKitServiceTests.DeleteZone.swift @@ -0,0 +1,38 @@ +// +// CloudKitServiceTests.DeleteZone.swift +// MistKit +// +// Created by Leo Dion. +// Copyright © 2026 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +internal import Foundation +internal import Testing + +@testable import MistKit + +extension CloudKitServiceTests { + @Suite("CloudKitService DeleteZone Operations", .enabled(if: Platform.isCryptoAvailable)) + internal enum DeleteZone {} +} diff --git a/Tests/MistKitTests/CloudKitService/FetchZoneChanges/CloudKitServiceTests.FetchZoneChanges+Helpers.swift b/Tests/MistKitTests/CloudKitService/FetchZoneChanges/CloudKitServiceTests.FetchZoneChanges+Helpers.swift index 01c2a419..1ef39252 100644 --- a/Tests/MistKitTests/CloudKitService/FetchZoneChanges/CloudKitServiceTests.FetchZoneChanges+Helpers.swift +++ b/Tests/MistKitTests/CloudKitService/FetchZoneChanges/CloudKitServiceTests.FetchZoneChanges+Helpers.swift @@ -39,10 +39,12 @@ extension CloudKitServiceTests.FetchZoneChanges { internal static func makeSuccessfulService( zoneCount: Int = 1, + moreComing: Bool = false, syncToken: String = "zone-sync-token-abc" ) async throws -> CloudKitService { let responseProvider = try ResponseProvider.successfulFetchZoneChanges( zoneCount: zoneCount, + moreComing: moreComing, syncToken: syncToken ) let transport = MockTransport(responseProvider: responseProvider) @@ -69,11 +71,13 @@ extension CloudKitServiceTests.FetchZoneChanges { extension ResponseProvider { internal static func successfulFetchZoneChanges( zoneCount: Int = 1, + moreComing: Bool = false, syncToken: String = "zone-sync-token-abc" ) throws -> ResponseProvider { ResponseProvider( defaultResponse: try .successfulFetchZoneChangesResponse( zoneCount: zoneCount, + moreComing: moreComing, syncToken: syncToken ) ) @@ -83,6 +87,7 @@ extension ResponseProvider { extension ResponseConfig { internal static func successfulFetchZoneChangesResponse( zoneCount: Int = 1, + moreComing: Bool = false, syncToken: String = "zone-sync-token-abc" ) throws -> ResponseConfig { var zones: [[String: Any]] = [] @@ -101,7 +106,8 @@ extension ResponseConfig { let responseJSON = """ { "zones": \(zonesString), - "syncToken": "\(syncToken)" + "syncToken": "\(syncToken)", + "moreComing": \(moreComing) } """ diff --git a/Tests/MistKitTests/CloudKitService/FetchZoneChanges/CloudKitServiceTests.FetchZoneChanges.SuccessCases+Pagination.swift b/Tests/MistKitTests/CloudKitService/FetchZoneChanges/CloudKitServiceTests.FetchZoneChanges.SuccessCases+Pagination.swift new file mode 100644 index 00000000..864534d1 --- /dev/null +++ b/Tests/MistKitTests/CloudKitService/FetchZoneChanges/CloudKitServiceTests.FetchZoneChanges.SuccessCases+Pagination.swift @@ -0,0 +1,148 @@ +// +// CloudKitServiceTests.FetchZoneChanges.SuccessCases+Pagination.swift +// MistKit +// +// Created by Leo Dion. +// Copyright © 2026 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +internal import Foundation +internal import Testing + +@testable import MistKit + +extension CloudKitServiceTests.FetchZoneChanges { + internal static func makePaginatedService( + pages: [(zoneCount: Int, syncToken: String)] + ) async throws -> CloudKitService { + let provider = ResponseProvider( + defaultResponse: try .successfulFetchZoneChangesResponse( + zoneCount: 0, + moreComing: false, + syncToken: "final-token" + ) + ) + for (index, page) in pages.enumerated() { + let moreComing = index < pages.count - 1 + await provider.enqueue( + try .successfulFetchZoneChangesResponse( + zoneCount: page.zoneCount, + moreComing: moreComing, + syncToken: page.syncToken + ), + for: "fetchZoneChanges" + ) + } + let transport = MockTransport(responseProvider: provider) + return try CloudKitService( + containerIdentifier: TestConstants.serviceContainerIdentifier, + credentials: Credentials( + apiAuth: APICredentials( + apiToken: TestConstants.apiToken, + webAuthToken: TestConstants.webAuthToken + ) + ), + transport: transport + ) + } + + internal static func makeStuckTokenService( + syncToken: String = "stuck-token" + ) async throws -> CloudKitService { + let responseProvider = ResponseProvider( + defaultResponse: try .successfulFetchZoneChangesResponse( + zoneCount: 0, + moreComing: true, + syncToken: syncToken + ) + ) + let transport = MockTransport(responseProvider: responseProvider) + return try CloudKitService( + containerIdentifier: TestConstants.serviceContainerIdentifier, + credentials: Credentials( + apiAuth: APICredentials( + apiToken: TestConstants.apiToken, + webAuthToken: TestConstants.webAuthToken + ) + ), + transport: transport + ) + } +} + +extension CloudKitServiceTests.FetchZoneChanges.SuccessCases { + @Test("fetchAllZoneChanges() handles moreComing=true with empty first page") + internal func fetchAllZoneChangesEmptyFirstPage() async throws { + guard #available(macOS 11.0, iOS 14.0, tvOS 14.0, watchOS 7.0, *) else { + Issue.record("CloudKitService is not available on this operating system.") + return + } + let service = try await CloudKitServiceTests.FetchZoneChanges.makePaginatedService(pages: [ + (zoneCount: 0, syncToken: "token-1"), + (zoneCount: 3, syncToken: "token-2"), + ]) + + let (zones, token) = try await service.fetchAllZoneChanges(database: .private) + + #expect(zones.count == 3) + #expect(token == "token-2") + } + + @Test("fetchAllZoneChanges() accumulates zones across three pages") + internal func fetchAllZoneChangesThreePage() async throws { + guard #available(macOS 11.0, iOS 14.0, tvOS 14.0, watchOS 7.0, *) else { + Issue.record("CloudKitService is not available on this operating system.") + return + } + let service = try await CloudKitServiceTests.FetchZoneChanges.makePaginatedService(pages: [ + (zoneCount: 2, syncToken: "token-1"), + (zoneCount: 3, syncToken: "token-2"), + (zoneCount: 2, syncToken: "token-3"), + ]) + + let (zones, token) = try await service.fetchAllZoneChanges(database: .private) + + #expect(zones.count == 7) + #expect(token == "token-3") + } + + @Test("fetchAllZoneChanges() breaks out when server returns stuck token with no zones") + internal func fetchAllZoneChangesEscapesStuckToken() async throws { + guard #available(macOS 11.0, iOS 14.0, tvOS 14.0, watchOS 7.0, *) else { + Issue.record("CloudKitService is not available on this operating system.") + return + } + let service = try await CloudKitServiceTests.FetchZoneChanges.makeStuckTokenService( + syncToken: "stuck-token" + ) + + let (zones, token) = try await service.fetchAllZoneChanges( + syncToken: "stuck-token", + database: .private + ) + + #expect(zones.isEmpty) + #expect(token == "stuck-token") + } +} From 2209b3ad13d97c89e09bb66c2587aefd09b761a9 Mon Sep 17 00:00:00 2001 From: Leo Dion Date: Tue, 19 May 2026 16:43:59 +0100 Subject: [PATCH 09/35] adding table of features --- docs/internals/authentication-middleware.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/docs/internals/authentication-middleware.md b/docs/internals/authentication-middleware.md index b4994036..bfb817ef 100644 --- a/docs/internals/authentication-middleware.md +++ b/docs/internals/authentication-middleware.md @@ -2,6 +2,18 @@ MistKit's authentication system uses an HTTP middleware pattern to transparently sign every request with the correct credentials, supporting three authentication methods and runtime upgrades between them. +## Database / Authentication Support + +CloudKit constrains which authentication mode is valid for each database scope. MistKit surfaces those constraints through `Database` and `PublicAuthPreference`: + +| Authentication mode | **Private** | **Shared** | **Public** | +|----------------------------------------------------|----------------------|----------------------|---------------------------------------------------------------------------| +| **API Token only** (`ckAPIToken`) | Not supported | Not supported | Whatever the public schema grants the `_world` role (typically reads of `/records/query`, `/records/lookup`) | +| **Web Auth** (`ckAPIToken` + `ckWebAuthToken`) | All endpoints | All endpoints | All endpoints — **only** mode accepted for `/users/*` | +| **Server-to-Server** (ECDSA P-256) | Rejected by CloudKit | Rejected by CloudKit | All endpoints **except** `/users/*` (CloudKit rejects S2S there) | + +Public-database calls pick attribution per-call via `PublicAuthPreference` on `Database.public(_:)` — see [Per-Call Attribution](#per-call-attribution-publicauthpreference). Private and shared scopes always require web-auth; CloudKit rejects server-to-server signatures on those endpoints. User-context routes (`/users/*`) always pass `.public(.requires(.webAuth))` because CloudKit only honors web-auth there. + ## TokenManager Protocol A `TokenManager` is the lifecycle owner of credentials (loading, validating, rotating, persisting). It vends an `Authenticator` to whomever needs to apply those credentials to an outgoing request: From 9c0791bfa2fc6c49af0ac655b566bafc63113937 Mon Sep 17 00:00:00 2001 From: leogdion Date: Tue, 19 May 2026 16:48:05 +0100 Subject: [PATCH 10/35] Phase 4: list-zones, modify-zones, discover, validate (#215) (#368) --- .../Commands/DiscoverCommand.swift | 84 +++++++++ .../Commands/ListZonesCommand.swift | 86 ++++++++++ .../Commands/ModifyZonesCommand.swift | 101 +++++++++++ .../Commands/ValidateCommand.swift | 161 ++++++++++++++++++ .../Configuration/DiscoverConfig.swift | 121 +++++++++++++ .../Configuration/ListZonesConfig.swift | 92 ++++++++++ ...MistDemoConfig+DatabaseConfiguration.swift | 7 + .../Configuration/ModifyZonesConfig.swift | 140 +++++++++++++++ .../Configuration/ValidateConfig.swift | 102 +++++++++++ .../Configuration/ZoneOperationInput.swift | 66 +++++++ .../ZoneOperationsEnvelope.swift | 42 +++++ .../MistDemoKit/Errors/DiscoverError.swift | 65 +++++++ .../MistDemoKit/Errors/ListZonesError.swift | 53 ++++++ .../MistDemoKit/Errors/ModifyZonesError.swift | 92 ++++++++++ .../MistDemoKit/Errors/ValidateError.swift | 49 ++++++ .../Integration/Phases/ModifyZonesPhase.swift | 103 +++++++++++ .../Tests/PrivateDatabaseTest.swift | 1 + .../Sources/MistDemoKit/MistDemoRunner.swift | 4 + .../MistDemoKit/Models/ValidationResult.swift | 71 ++++++++ .../Commands/DiscoverCommandTests.swift | 84 +++++++++ .../Commands/ListZonesCommandTests.swift | 77 +++++++++ .../Commands/ModifyZonesCommandTests.swift | 135 +++++++++++++++ .../Commands/ValidateCommandTests.swift | 93 ++++++++++ 23 files changed, 1829 insertions(+) create mode 100644 Examples/MistDemo/Sources/MistDemoKit/Commands/DiscoverCommand.swift create mode 100644 Examples/MistDemo/Sources/MistDemoKit/Commands/ListZonesCommand.swift create mode 100644 Examples/MistDemo/Sources/MistDemoKit/Commands/ModifyZonesCommand.swift create mode 100644 Examples/MistDemo/Sources/MistDemoKit/Commands/ValidateCommand.swift create mode 100644 Examples/MistDemo/Sources/MistDemoKit/Configuration/DiscoverConfig.swift create mode 100644 Examples/MistDemo/Sources/MistDemoKit/Configuration/ListZonesConfig.swift create mode 100644 Examples/MistDemo/Sources/MistDemoKit/Configuration/ModifyZonesConfig.swift create mode 100644 Examples/MistDemo/Sources/MistDemoKit/Configuration/ValidateConfig.swift create mode 100644 Examples/MistDemo/Sources/MistDemoKit/Configuration/ZoneOperationInput.swift create mode 100644 Examples/MistDemo/Sources/MistDemoKit/Configuration/ZoneOperationsEnvelope.swift create mode 100644 Examples/MistDemo/Sources/MistDemoKit/Errors/DiscoverError.swift create mode 100644 Examples/MistDemo/Sources/MistDemoKit/Errors/ListZonesError.swift create mode 100644 Examples/MistDemo/Sources/MistDemoKit/Errors/ModifyZonesError.swift create mode 100644 Examples/MistDemo/Sources/MistDemoKit/Errors/ValidateError.swift create mode 100644 Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/ModifyZonesPhase.swift create mode 100644 Examples/MistDemo/Sources/MistDemoKit/Models/ValidationResult.swift create mode 100644 Examples/MistDemo/Tests/MistDemoTests/Commands/DiscoverCommandTests.swift create mode 100644 Examples/MistDemo/Tests/MistDemoTests/Commands/ListZonesCommandTests.swift create mode 100644 Examples/MistDemo/Tests/MistDemoTests/Commands/ModifyZonesCommandTests.swift create mode 100644 Examples/MistDemo/Tests/MistDemoTests/Commands/ValidateCommandTests.swift diff --git a/Examples/MistDemo/Sources/MistDemoKit/Commands/DiscoverCommand.swift b/Examples/MistDemo/Sources/MistDemoKit/Commands/DiscoverCommand.swift new file mode 100644 index 00000000..5a251d3e --- /dev/null +++ b/Examples/MistDemo/Sources/MistDemoKit/Commands/DiscoverCommand.swift @@ -0,0 +1,84 @@ +// +// DiscoverCommand.swift +// MistDemo +// +// Created by Leo Dion. +// Copyright © 2026 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +internal import Foundation +internal import MistKit + +/// Command that discovers user identities by email address. +public struct DiscoverCommand: MistDemoCommand, OutputFormatting { + /// The configuration type. + public typealias Config = DiscoverConfig + /// The command name. + public static let commandName = "discover" + /// The command abstract. + public static let abstract = "Discover user identities by email" + /// The command help text. + public static let helpText = """ + DISCOVER - Discover user identities by email + + USAGE: + mistdemo discover --discover-emails + cat emails.txt | mistdemo discover --stdin + + INPUT (choose one): + --discover-emails Comma-separated email addresses + --stdin Read one email per line from stdin + + OPTIONS: + --output-format Output format (json, table, csv, yaml) + + NOTES: + - Requires API + web-auth credentials. The underlying CloudKit + endpoint is pinned to the public database; the database flag + does not apply. + - CloudKit only returns identities for accounts discoverable to + the caller. + """ + + private let config: DiscoverConfig + + /// Creates a new instance. + public init(config: DiscoverConfig) { + self.config = config + } + + /// Executes the command. + public func execute() async throws { + guard !config.emails.isEmpty else { + throw DiscoverError.emailsRequired + } + guard config.base.hasUserContextCredentials else { + throw DiscoverError.webAuthRequired + } + + let service = try MistKitClientFactory.create(for: config.base) + let identities = try await service.lookupUsersByEmail(config.emails) + try await outputResults(identities, format: config.output) + } +} diff --git a/Examples/MistDemo/Sources/MistDemoKit/Commands/ListZonesCommand.swift b/Examples/MistDemo/Sources/MistDemoKit/Commands/ListZonesCommand.swift new file mode 100644 index 00000000..8543c2df --- /dev/null +++ b/Examples/MistDemo/Sources/MistDemoKit/Commands/ListZonesCommand.swift @@ -0,0 +1,86 @@ +// +// ListZonesCommand.swift +// MistDemo +// +// Created by Leo Dion. +// Copyright © 2026 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +internal import Foundation +internal import MistKit + +/// Command that lists all zones in the configured database. +public struct ListZonesCommand: MistDemoCommand, OutputFormatting { + /// The configuration type. + public typealias Config = ListZonesConfig + /// The command name. + public static let commandName = "list-zones" + /// The command abstract. + public static let abstract = "List all zones in the database" + /// The command help text. + public static let helpText = """ + LIST-ZONES - List all zones in the database + + USAGE: + mistdemo list-zones [options] + + OPTIONS: + --database Database to target (private, shared) + --zones-include-default Include `_defaultZone` in the output + --output-format Output format (json, table, csv, yaml) + + EXAMPLES: + mistdemo list-zones --database private + mistdemo list-zones --database shared --zones-include-default + + NOTES: + - Only `private` and `shared` databases support zone listing. + - By default the default zone (`_defaultZone`) is filtered out so + only custom zones are shown. + """ + + private let config: ListZonesConfig + + /// Creates a new instance. + public init(config: ListZonesConfig) { + self.config = config + } + + /// Executes the command. + public func execute() async throws { + if case .public = config.base.database { + throw ListZonesError.databaseNotSupported + } + + let service = try MistKitClientFactory.create(for: config.base) + let zones = try await service.listZones(database: config.base.database) + + let filtered = + config.includeDefault + ? zones + : zones.filter { $0.zoneName != ZoneID.defaultZone.zoneName } + + try await outputResults(filtered, format: config.output) + } +} diff --git a/Examples/MistDemo/Sources/MistDemoKit/Commands/ModifyZonesCommand.swift b/Examples/MistDemo/Sources/MistDemoKit/Commands/ModifyZonesCommand.swift new file mode 100644 index 00000000..04dfb05e --- /dev/null +++ b/Examples/MistDemo/Sources/MistDemoKit/Commands/ModifyZonesCommand.swift @@ -0,0 +1,101 @@ +// +// ModifyZonesCommand.swift +// MistDemo +// +// Created by Leo Dion. +// Copyright © 2026 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +internal import Foundation +internal import MistKit + +/// Command that creates or deletes zones. +public struct ModifyZonesCommand: MistDemoCommand, OutputFormatting { + /// The configuration type. + public typealias Config = ModifyZonesConfig + /// The command name. + public static let commandName = "modify-zones" + /// The command abstract. + public static let abstract = "Create or delete CloudKit zones" + /// The command help text. + public static let helpText = """ + MODIFY-ZONES - Create or delete CloudKit zones + + USAGE: + mistdemo modify-zones --operations-file [options] + cat zones.json | mistdemo modify-zones --stdin [options] + + INPUT (choose one): + --operations-file Path to JSON envelope + --stdin Read JSON envelope from stdin + + OPTIONS: + --database Database to target (private, shared) + --output-format Output format (json, table, csv, yaml) + + INPUT FORMAT: + { + "operations": [ + { "type": "create", "zoneName": "Articles" }, + { "type": "delete", "zoneName": "Archive" } + ] + } + + NOTES: + - Only `private` and `shared` databases support zone modification. + - Each delete is announced on stderr before the request is sent. + """ + + private let config: ModifyZonesConfig + + /// Creates a new instance. + public init(config: ModifyZonesConfig) { + self.config = config + } + + /// Executes the command. + public func execute() async throws { + if case .public = config.base.database { + throw ModifyZonesError.databaseNotSupported + } + + let service = try MistKitClientFactory.create(for: config.base) + + let operations = try config.operations.map { input -> ZoneOperation in + let operation = try input.toZoneOperation() + if case .delete(let zoneID) = operation { + let warning = "⚠️ Deleting zone '\(zoneID.zoneName)'\n" + FileHandle.standardError.write(Data(warning.utf8)) + } + return operation + } + + let results = try await service.modifyZones( + operations, + database: config.base.database + ) + + try await outputResults(results, format: config.output) + } +} diff --git a/Examples/MistDemo/Sources/MistDemoKit/Commands/ValidateCommand.swift b/Examples/MistDemo/Sources/MistDemoKit/Commands/ValidateCommand.swift new file mode 100644 index 00000000..0a3e7e8f --- /dev/null +++ b/Examples/MistDemo/Sources/MistDemoKit/Commands/ValidateCommand.swift @@ -0,0 +1,161 @@ +// +// ValidateCommand.swift +// MistDemo +// +// Created by Leo Dion. +// Copyright © 2026 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +internal import Foundation +internal import MistKit + +/// Command that validates the local CloudKit credential configuration and +/// optionally exercises a live API round-trip. +public struct ValidateCommand: MistDemoCommand, OutputFormatting { + /// The configuration type. + public typealias Config = ValidateConfig + /// The command name. + public static let commandName = "validate" + /// The command abstract. + public static let abstract = "Validate CloudKit credentials and reachability" + /// The command help text. + public static let helpText = """ + VALIDATE - Validate CloudKit credentials and reachability + + USAGE: + mistdemo validate [options] + + OPTIONS: + --validate-skip-network Only parse credentials; skip network call. + --validate-test-query Also run a minimal listZones query. + --output-format Output format (json, table, csv, yaml). + + EXIT CODES: + 0 Validation succeeded. + 1 One or more checks failed; see structured `errors` in output. + + NOTES: + - `fetchCaller` (the network check) requires API + web-auth + credentials. With server-to-server only, the network check is + skipped automatically. + - With --validate.skip-network, only the parse-time check runs. + """ + + private let config: ValidateConfig + + /// Creates a new instance. + public init(config: ValidateConfig) { + self.config = config + } + + /// Executes the command. + public func execute() async throws { + var errors: [String] = [] + let service = makeService(into: &errors) + let userInfo = await fetchCallerIfPossible( + service: service, + errors: &errors + ) + let zonesFound = await runTestQueryIfRequested( + service: service, + errors: &errors + ) + + let result = ValidationResult( + credentialsValid: service != nil, + webAuthConfigured: config.base.hasUserContextCredentials, + serverToServerConfigured: config.base.hasServerToServerCredentials, + userInfo: userInfo, + zonesFound: zonesFound, + errors: errors + ) + try await emit(result) + + if !errors.isEmpty { + throw ValidateError(reason: errors.joined(separator: "; ")) + } + } + + /// Emit the validation result. JSON output is written through + /// `FileHandle.standardOutput` directly so callers piping the output + /// still see structured JSON even when this command throws afterwards + /// (Swift's `print()` is fully buffered when stdout is not a TTY, and + /// the fatal-error path that handles the throw doesn't flush the buffer). + private func emit(_ result: ValidationResult) async throws { + guard config.output == .json else { + try await outputResult(result, format: config.output) + return + } + let encoder = JSONEncoder() + encoder.outputFormatting = [.prettyPrinted, .sortedKeys] + let data = try encoder.encode(result) + FileHandle.standardOutput.write(data) + FileHandle.standardOutput.write(Data("\n".utf8)) + } + + private func makeService(into errors: inout [String]) -> CloudKitService? { + do { + return try MistKitClientFactory.create(for: config.base) + } catch { + errors.append(error.localizedDescription) + return nil + } + } + + private func fetchCallerIfPossible( + service: CloudKitService?, + errors: inout [String] + ) async -> UserInfo? { + guard let service, + !config.skipNetwork, + config.base.hasUserContextCredentials + else { + return nil + } + do { + return try await service.fetchCaller() + } catch { + errors.append("fetchCaller failed: \(error.localizedDescription)") + return nil + } + } + + private func runTestQueryIfRequested( + service: CloudKitService?, + errors: inout [String] + ) async -> Int? { + guard let service, config.testQuery else { + return nil + } + do { + let zones = try await service.listZones(database: config.base.database) + return zones.count + } catch { + errors.append( + "Test query (listZones) failed: \(error.localizedDescription)" + ) + return nil + } + } +} diff --git a/Examples/MistDemo/Sources/MistDemoKit/Configuration/DiscoverConfig.swift b/Examples/MistDemo/Sources/MistDemoKit/Configuration/DiscoverConfig.swift new file mode 100644 index 00000000..825e2bde --- /dev/null +++ b/Examples/MistDemo/Sources/MistDemoKit/Configuration/DiscoverConfig.swift @@ -0,0 +1,121 @@ +// +// DiscoverConfig.swift +// MistDemo +// +// Created by Leo Dion. +// Copyright © 2026 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +public import ConfigKeyKit +internal import Foundation + +/// Configuration for the `discover` command (email lookup). +public struct DiscoverConfig: Sendable, ConfigurationParseable { + /// The configuration reader type. + public typealias ConfigReader = MistDemoConfiguration + /// The base configuration type. + public typealias BaseConfig = MistDemoConfig + + /// The base MistDemo configuration. + public let base: MistDemoConfig + /// The email addresses to look up. + public let emails: [String] + /// The output format. + public let output: OutputFormat + + /// Creates a new instance. + public init( + base: MistDemoConfig, + emails: [String], + output: OutputFormat = .json + ) { + self.base = base + self.emails = emails + self.output = output + } + + /// Parse configuration from command line arguments. + public init( + configuration: MistDemoConfiguration, + base: MistDemoConfig? + ) async throws { + let baseConfig: MistDemoConfig + if let base { + baseConfig = base + } else { + baseConfig = try await MistDemoConfig( + configuration: configuration, + base: nil + ) + } + + let emails = Self.parseEmails(from: configuration) + + let outputString = + configuration.string( + forKey: MistDemoConstants.ConfigKeys.outputFormat, + default: MistDemoConstants.Defaults.outputFormat + ) ?? MistDemoConstants.Defaults.outputFormat + let output = OutputFormat(rawValue: outputString) ?? .json + + self.init( + base: baseConfig, + emails: emails, + output: output + ) + } + + /// Parse emails from the `discover.emails` key (comma-separated) or stdin + /// (one address per line) when `--stdin` is set. + internal static func parseEmails( + from configuration: MistDemoConfiguration + ) -> [String] { + if let raw = configuration.string(forKey: "discover.emails"), + !raw.isEmpty + { + return + raw + .split(separator: ",") + .map { String($0).trimmingCharacters(in: .whitespaces) } + .filter { !$0.isEmpty } + } + + if configuration.bool( + forKey: MistDemoConstants.ConfigKeys.stdin, + default: false + ) { + let stdinData = FileHandle.standardInput.readDataToEndOfFile() + guard let raw = String(data: stdinData, encoding: .utf8) else { + return [] + } + return + raw + .split(whereSeparator: { $0.isNewline }) + .map { String($0).trimmingCharacters(in: .whitespaces) } + .filter { !$0.isEmpty } + } + + return [] + } +} diff --git a/Examples/MistDemo/Sources/MistDemoKit/Configuration/ListZonesConfig.swift b/Examples/MistDemo/Sources/MistDemoKit/Configuration/ListZonesConfig.swift new file mode 100644 index 00000000..d4206f9e --- /dev/null +++ b/Examples/MistDemo/Sources/MistDemoKit/Configuration/ListZonesConfig.swift @@ -0,0 +1,92 @@ +// +// ListZonesConfig.swift +// MistDemo +// +// Created by Leo Dion. +// Copyright © 2026 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +public import ConfigKeyKit +internal import Foundation + +/// Configuration for the `list-zones` command. +public struct ListZonesConfig: Sendable, ConfigurationParseable { + /// The configuration reader type. + public typealias ConfigReader = MistDemoConfiguration + /// The base configuration type. + public typealias BaseConfig = MistDemoConfig + + /// The base MistDemo configuration. + public let base: MistDemoConfig + /// If true, include `_defaultZone` in the listing; otherwise it's filtered + /// out so only custom zones are shown. + public let includeDefault: Bool + /// The output format. + public let output: OutputFormat + + /// Creates a new instance. + public init( + base: MistDemoConfig, + includeDefault: Bool = false, + output: OutputFormat = .table + ) { + self.base = base + self.includeDefault = includeDefault + self.output = output + } + + /// Parse configuration from command line arguments. + public init( + configuration: MistDemoConfiguration, + base: MistDemoConfig? + ) async throws { + let baseConfig: MistDemoConfig + if let base { + baseConfig = base + } else { + baseConfig = try await MistDemoConfig( + configuration: configuration, + base: nil + ) + } + + let includeDefault = configuration.bool( + forKey: "zones.include-default", + default: false + ) + + let outputString = + configuration.string( + forKey: MistDemoConstants.ConfigKeys.outputFormat, + default: "table" + ) ?? "table" + let output = OutputFormat(rawValue: outputString) ?? .table + + self.init( + base: baseConfig, + includeDefault: includeDefault, + output: output + ) + } +} diff --git a/Examples/MistDemo/Sources/MistDemoKit/Configuration/MistDemoConfig+DatabaseConfiguration.swift b/Examples/MistDemo/Sources/MistDemoKit/Configuration/MistDemoConfig+DatabaseConfiguration.swift index 243e5f58..36d4f673 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Configuration/MistDemoConfig+DatabaseConfiguration.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Configuration/MistDemoConfig+DatabaseConfiguration.swift @@ -40,6 +40,13 @@ extension MistDemoConfig { (try? resolveAPICredentials()) != nil } + /// Indicates whether server-to-server signing material (key ID + private + /// key) is present in the configuration. Required to sign `.public` + /// database requests. + internal var hasServerToServerCredentials: Bool { + (try? resolveServerToServerCredentials()) != nil + } + /// Build `Credentials` for the primary `CloudKitService` targeting /// `self.database`. /// diff --git a/Examples/MistDemo/Sources/MistDemoKit/Configuration/ModifyZonesConfig.swift b/Examples/MistDemo/Sources/MistDemoKit/Configuration/ModifyZonesConfig.swift new file mode 100644 index 00000000..b9a9d8f7 --- /dev/null +++ b/Examples/MistDemo/Sources/MistDemoKit/Configuration/ModifyZonesConfig.swift @@ -0,0 +1,140 @@ +// +// ModifyZonesConfig.swift +// MistDemo +// +// Created by Leo Dion. +// Copyright © 2026 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +public import ConfigKeyKit +public import Foundation + +/// Configuration for the `modify-zones` command. +public struct ModifyZonesConfig: Sendable, ConfigurationParseable { + /// The configuration reader type. + public typealias ConfigReader = MistDemoConfiguration + /// The base configuration type. + public typealias BaseConfig = MistDemoConfig + + /// The base MistDemo configuration. + public let base: MistDemoConfig + /// The list of zone operations to perform. + public let operations: [ZoneOperationInput] + /// The output format. + public let output: OutputFormat + + /// Creates a new instance. + public init( + base: MistDemoConfig, + operations: [ZoneOperationInput], + output: OutputFormat = .json + ) { + self.base = base + self.operations = operations + self.output = output + } + + /// Parse configuration from command line arguments. + public init( + configuration: MistDemoConfiguration, + base: MistDemoConfig? + ) async throws { + let baseConfig: MistDemoConfig + if let base { + baseConfig = base + } else { + baseConfig = try await MistDemoConfig( + configuration: configuration, + base: nil + ) + } + + let operations = try Self.parseOperationsFromSources(configuration) + + let outputString = + configuration.string( + forKey: MistDemoConstants.ConfigKeys.outputFormat, + default: MistDemoConstants.Defaults.outputFormat + ) ?? MistDemoConstants.Defaults.outputFormat + let output = OutputFormat(rawValue: outputString) ?? .json + + self.init( + base: baseConfig, + operations: operations, + output: output + ) + } + + /// Parse a zone-operations JSON envelope from data. + /// + /// Accepts the envelope form `{ "operations": [...] }`. + public static func parseOperations( + from data: Data + ) throws -> [ZoneOperationInput] { + do { + let envelope = try JSONDecoder().decode( + ZoneOperationsEnvelope.self, + from: data + ) + return envelope.operations + } catch let error as ModifyZonesError { + throw error + } catch { + throw ModifyZonesError.parsingFailed(error.localizedDescription) + } + } + + private static func parseOperationsFromSources( + _ configReader: MistDemoConfiguration + ) throws -> [ZoneOperationInput] { + if let path = configReader.string( + forKey: MistDemoConstants.ConfigKeys.operationsFile + ) { + do { + let data = try Data(contentsOf: URL(fileURLWithPath: path)) + return try parseOperations(from: data) + } catch let error as ModifyZonesError { + throw error + } catch { + throw ModifyZonesError.operationsFileError( + path, + error.localizedDescription + ) + } + } + + if configReader.bool( + forKey: MistDemoConstants.ConfigKeys.stdin, + default: false + ) { + let stdinData = FileHandle.standardInput.readDataToEndOfFile() + guard !stdinData.isEmpty else { + throw ModifyZonesError.emptyStdin + } + return try parseOperations(from: stdinData) + } + + throw ModifyZonesError.operationsRequired + } +} diff --git a/Examples/MistDemo/Sources/MistDemoKit/Configuration/ValidateConfig.swift b/Examples/MistDemo/Sources/MistDemoKit/Configuration/ValidateConfig.swift new file mode 100644 index 00000000..09a361c1 --- /dev/null +++ b/Examples/MistDemo/Sources/MistDemoKit/Configuration/ValidateConfig.swift @@ -0,0 +1,102 @@ +// +// ValidateConfig.swift +// MistDemo +// +// Created by Leo Dion. +// Copyright © 2026 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +public import ConfigKeyKit +internal import Foundation + +/// Configuration for the `validate` command. +public struct ValidateConfig: Sendable, ConfigurationParseable { + /// The configuration reader type. + public typealias ConfigReader = MistDemoConfiguration + /// The base configuration type. + public typealias BaseConfig = MistDemoConfig + + /// The base MistDemo configuration. + public let base: MistDemoConfig + /// If true, only check that credentials parse — skip the live `fetchCaller` + /// round-trip. + public let skipNetwork: Bool + /// If true, also run a minimal `listZones` query against the configured + /// database to verify end-to-end reachability. + public let testQuery: Bool + /// The output format. + public let output: OutputFormat + + /// Creates a new instance. + public init( + base: MistDemoConfig, + skipNetwork: Bool = false, + testQuery: Bool = false, + output: OutputFormat = .json + ) { + self.base = base + self.skipNetwork = skipNetwork + self.testQuery = testQuery + self.output = output + } + + /// Parse configuration from command line arguments. + public init( + configuration: MistDemoConfiguration, + base: MistDemoConfig? + ) async throws { + let baseConfig: MistDemoConfig + if let base { + baseConfig = base + } else { + baseConfig = try await MistDemoConfig( + configuration: configuration, + base: nil + ) + } + + let skipNetwork = configuration.bool( + forKey: "validate.skip-network", + default: false + ) + let testQuery = configuration.bool( + forKey: "validate.test-query", + default: false + ) + + let outputString = + configuration.string( + forKey: MistDemoConstants.ConfigKeys.outputFormat, + default: MistDemoConstants.Defaults.outputFormat + ) ?? MistDemoConstants.Defaults.outputFormat + let output = OutputFormat(rawValue: outputString) ?? .json + + self.init( + base: baseConfig, + skipNetwork: skipNetwork, + testQuery: testQuery, + output: output + ) + } +} diff --git a/Examples/MistDemo/Sources/MistDemoKit/Configuration/ZoneOperationInput.swift b/Examples/MistDemo/Sources/MistDemoKit/Configuration/ZoneOperationInput.swift new file mode 100644 index 00000000..3e68dbee --- /dev/null +++ b/Examples/MistDemo/Sources/MistDemoKit/Configuration/ZoneOperationInput.swift @@ -0,0 +1,66 @@ +// +// ZoneOperationInput.swift +// MistDemo +// +// Created by Leo Dion. +// Copyright © 2026 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +public import Foundation +public import MistKit + +/// One zone operation parsed from the modify-zones JSON payload. +public struct ZoneOperationInput: Codable, Sendable, Equatable { + /// The operation kind ("create" or "delete"). + public let type: String + /// The CloudKit zone name. + public let zoneName: String + + /// Creates a new instance. + public init(type: String, zoneName: String) { + self.type = type + self.zoneName = zoneName + } + + /// Convert this operation input into a MistKit `ZoneOperation`. + /// + /// - Throws: `ModifyZonesError.invalidOperationType` for unknown `type` + /// values, or `.invalidZoneName` for empty / whitespace-only names. + public func toZoneOperation() throws -> ZoneOperation { + let trimmed = zoneName.trimmingCharacters(in: .whitespaces) + guard !trimmed.isEmpty else { + throw ModifyZonesError.invalidZoneName(zoneName) + } + let zoneID = ZoneID(zoneName: trimmed, ownerName: nil) + + switch type.lowercased() { + case "create": + return .create(zoneID) + case "delete": + return .delete(zoneID) + default: + throw ModifyZonesError.invalidOperationType(type) + } + } +} diff --git a/Examples/MistDemo/Sources/MistDemoKit/Configuration/ZoneOperationsEnvelope.swift b/Examples/MistDemo/Sources/MistDemoKit/Configuration/ZoneOperationsEnvelope.swift new file mode 100644 index 00000000..d6c53588 --- /dev/null +++ b/Examples/MistDemo/Sources/MistDemoKit/Configuration/ZoneOperationsEnvelope.swift @@ -0,0 +1,42 @@ +// +// ZoneOperationsEnvelope.swift +// MistDemo +// +// Created by Leo Dion. +// Copyright © 2026 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +internal import Foundation + +/// JSON envelope for the `modify-zones` payload: +/// `{ "operations": [ZoneOperationInput] }`. +public struct ZoneOperationsEnvelope: Codable, Sendable { + /// The list of zone operations. + public let operations: [ZoneOperationInput] + + /// Creates a new instance. + public init(operations: [ZoneOperationInput]) { + self.operations = operations + } +} diff --git a/Examples/MistDemo/Sources/MistDemoKit/Errors/DiscoverError.swift b/Examples/MistDemo/Sources/MistDemoKit/Errors/DiscoverError.swift new file mode 100644 index 00000000..b78e7f9f --- /dev/null +++ b/Examples/MistDemo/Sources/MistDemoKit/Errors/DiscoverError.swift @@ -0,0 +1,65 @@ +// +// DiscoverError.swift +// MistDemo +// +// Created by Leo Dion. +// Copyright © 2026 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +public import Foundation + +/// Errors that can occur during discover command execution. +public enum DiscoverError: Error, LocalizedError { + case emailsRequired + case webAuthRequired + + /// A localized description of the error. + public var errorDescription: String? { + switch self { + case .emailsRequired: + return + "No emails provided. Use --discover-emails or pipe " + + "one address per line to stdin." + case .webAuthRequired: + return + "discover requires API + web-auth credentials. Set " + + "CLOUDKIT_API_TOKEN and CLOUDKIT_WEB_AUTH_TOKEN, or run " + + "`mistdemo auth-token` first." + } + } + + /// A localized recovery suggestion. + public var recoverySuggestion: String? { + switch self { + case .emailsRequired: + return + "Pass --discover-emails alice@example.com,bob@example.com, " + + "or pipe addresses to stdin with --stdin." + case .webAuthRequired: + return + "User-identity routes (lookupUsersByEmail) are pinned to " + + "CloudKit's public DB and require web-auth." + } + } +} diff --git a/Examples/MistDemo/Sources/MistDemoKit/Errors/ListZonesError.swift b/Examples/MistDemo/Sources/MistDemoKit/Errors/ListZonesError.swift new file mode 100644 index 00000000..17c1f1ad --- /dev/null +++ b/Examples/MistDemo/Sources/MistDemoKit/Errors/ListZonesError.swift @@ -0,0 +1,53 @@ +// +// ListZonesError.swift +// MistDemo +// +// Created by Leo Dion. +// Copyright © 2026 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +public import Foundation + +/// Errors that can occur during list-zones command execution. +public enum ListZonesError: Error, LocalizedError { + case databaseNotSupported + + /// A localized description of the error. + public var errorDescription: String? { + switch self { + case .databaseNotSupported: + return + "Zone listing requires --database private or --database shared. " + + "CloudKit's public database has no enumerable zones." + } + } + + /// A localized recovery suggestion. + public var recoverySuggestion: String? { + switch self { + case .databaseNotSupported: + return "Rerun with --database private or --database shared." + } + } +} diff --git a/Examples/MistDemo/Sources/MistDemoKit/Errors/ModifyZonesError.swift b/Examples/MistDemo/Sources/MistDemoKit/Errors/ModifyZonesError.swift new file mode 100644 index 00000000..4f79f3c3 --- /dev/null +++ b/Examples/MistDemo/Sources/MistDemoKit/Errors/ModifyZonesError.swift @@ -0,0 +1,92 @@ +// +// ModifyZonesError.swift +// MistDemo +// +// Created by Leo Dion. +// Copyright © 2026 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +public import Foundation + +/// Errors that can occur during modify-zones command execution. +public enum ModifyZonesError: Error, LocalizedError { + case databaseNotSupported + case operationsRequired + case operationsFileError(String, String) + case emptyStdin + case parsingFailed(String) + case invalidOperationType(String) + case invalidZoneName(String) + + /// A localized description of the error. + public var errorDescription: String? { + switch self { + case .databaseNotSupported: + return + "Zone modification requires --database private or --database shared. " + + "CloudKit's public database has no user-modifiable zones." + case .operationsRequired: + return + "No operations provided. " + + "Use --operations-file or pipe JSON to stdin." + case .operationsFileError(let path, let reason): + return "Failed to read operations file '\(path)': \(reason)" + case .emptyStdin: + return "Empty stdin. Provide a JSON object with `operations`." + case .parsingFailed(let reason): + return "Failed to parse zone operations: \(reason)" + case .invalidOperationType(let opType): + return + "Unknown zone operation type '\(opType)'. " + + "Use one of: create, delete." + case .invalidZoneName(let name): + return "Invalid zone name '\(name)'." + } + } + + /// A localized recovery suggestion. + public var recoverySuggestion: String? { + switch self { + case .databaseNotSupported: + return "Rerun with --database private or --database shared." + case .operationsRequired: + return + "Provide JSON: --operations-file ops.json or " + + "echo '{\"operations\":[...]}' | mistdemo modify-zones --stdin" + case .operationsFileError: + return "Ensure the file exists and contains valid JSON." + case .emptyStdin: + return + "Pipe JSON: echo " + + "'{\"operations\":[{\"type\":\"create\",\"zoneName\":\"X\"}]}' " + + "| mistdemo modify-zones --stdin" + case .parsingFailed: + return "Check the JSON syntax and the schema of each operation." + case .invalidOperationType: + return "Set 'type' to 'create' or 'delete'." + case .invalidZoneName: + return "Provide a non-empty zone name without leading/trailing whitespace." + } + } +} diff --git a/Examples/MistDemo/Sources/MistDemoKit/Errors/ValidateError.swift b/Examples/MistDemo/Sources/MistDemoKit/Errors/ValidateError.swift new file mode 100644 index 00000000..467601e2 --- /dev/null +++ b/Examples/MistDemo/Sources/MistDemoKit/Errors/ValidateError.swift @@ -0,0 +1,49 @@ +// +// ValidateError.swift +// MistDemo +// +// Created by Leo Dion. +// Copyright © 2026 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +public import Foundation + +/// Signals that the `validate` command's full pipeline did not pass. The +/// per-check failure messages are joined into `reason` and are also present +/// in the structured `ValidationResult.errors` already emitted to stdout — +/// this error exists purely to drive a non-zero exit code. +public struct ValidateError: Error, LocalizedError { + /// The joined reason(s) from the individual validation checks. + public let reason: String + + /// A localized description of the error. + public var errorDescription: String? { + "Validation failed: \(reason)" + } + + /// Creates a new instance. + public init(reason: String) { + self.reason = reason + } +} diff --git a/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/ModifyZonesPhase.swift b/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/ModifyZonesPhase.swift new file mode 100644 index 00000000..c3626fc5 --- /dev/null +++ b/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/ModifyZonesPhase.swift @@ -0,0 +1,103 @@ +// +// ModifyZonesPhase.swift +// MistDemo +// +// Created by Leo Dion. +// Copyright © 2026 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +internal import Foundation +internal import MistKit + +/// Exercises `modifyZones` end-to-end: create a uniquely-named test zone, +/// verify it via `lookupZones`, then delete it. Cleanup runs even on +/// verification failure so we don't leave stray zones behind. +internal struct ModifyZonesPhase: IntegrationPhase { + internal typealias Input = NoState + internal typealias Output = NoState + + internal static let title = "Modify zones (create + verify + delete)" + internal static let emoji = "🧱" + internal static let apiName = "modifyZones" + + internal func run( + input: NoState, + context: PhaseContext + ) async throws -> NoState { + print("\n\(Self.emoji) \(Self.title)") + + let suffix = UUID().uuidString.prefix(8) + let zoneName = "MistDemoIntegrationZone-\(suffix)" + let zoneID = ZoneID(zoneName: zoneName, ownerName: nil) + + do { + _ = try await context.service.modifyZones( + [.create(zoneID)], + database: context.database + ) + if context.verbose { + print(" ✅ Created zone: \(zoneName)") + } + + let lookedUp = try await context.service.lookupZones( + zoneIDs: [zoneID], + database: context.database + ) + guard lookedUp.contains(where: { $0.zoneName == zoneName }) else { + try await cleanup(zoneID: zoneID, context: context) + throw IntegrationTestError.verificationFailed( + "created zone '\(zoneName)' not returned by lookupZones" + ) + } + if context.verbose { + print(" ✅ Verified zone via lookupZones") + } + + try await cleanup(zoneID: zoneID, context: context) + if context.verbose { + print(" ✅ Deleted zone: \(zoneName)") + } + } catch let error as IntegrationTestError { + throw error + } catch { + try? await cleanup(zoneID: zoneID, context: context) + throw IntegrationTestError.verificationFailed( + "modifyZones round-trip failed: \(error.localizedDescription)" + ) + } + + print("✅ Round-tripped zone create/verify/delete") + return NoState() + } + + private func cleanup( + zoneID: ZoneID, + context: PhaseContext + ) async throws { + _ = try await context.service.modifyZones( + [.delete(zoneID)], + database: context.database + ) + } +} diff --git a/Examples/MistDemo/Sources/MistDemoKit/Integration/Tests/PrivateDatabaseTest.swift b/Examples/MistDemo/Sources/MistDemoKit/Integration/Tests/PrivateDatabaseTest.swift index c754b6e7..2e1790dc 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Integration/Tests/PrivateDatabaseTest.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Integration/Tests/PrivateDatabaseTest.swift @@ -41,6 +41,7 @@ internal struct PrivateDatabaseTest: PhasedIntegrationTest { // pipeline; the service resolves web-auth credentials per call when needed. internal let phases: [any IntegrationPhase] = [ ListZonesPhase(), + ModifyZonesPhase(), LookupZonePhase(), ZoneRoundtripPhase(), FetchZoneChangesPhase(), diff --git a/Examples/MistDemo/Sources/MistDemoKit/MistDemoRunner.swift b/Examples/MistDemo/Sources/MistDemoKit/MistDemoRunner.swift index 9df96a48..4ce77776 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/MistDemoRunner.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/MistDemoRunner.swift @@ -55,6 +55,10 @@ public enum MistDemoRunner { await registry.register(DemoInFilterCommand.self) await registry.register(LookupZonesCommand.self) await registry.register(CreateZoneCommand.self) + await registry.register(ListZonesCommand.self) + await registry.register(ModifyZonesCommand.self) + await registry.register(DiscoverCommand.self) + await registry.register(ValidateCommand.self) await registry.register(DeleteZoneCommand.self) await registry.register(FetchChangesCommand.self) await registry.register(TestPublicCommand.self) diff --git a/Examples/MistDemo/Sources/MistDemoKit/Models/ValidationResult.swift b/Examples/MistDemo/Sources/MistDemoKit/Models/ValidationResult.swift new file mode 100644 index 00000000..2651c61b --- /dev/null +++ b/Examples/MistDemo/Sources/MistDemoKit/Models/ValidationResult.swift @@ -0,0 +1,71 @@ +// +// ValidationResult.swift +// MistDemo +// +// Created by Leo Dion. +// Copyright © 2026 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +internal import Foundation +public import MistKit + +/// Structured outcome of the `validate` command. +public struct ValidationResult: Encodable, Sendable { + /// Whether MistDemo successfully parsed the configured credentials into a + /// `CloudKitService`. False indicates a configuration error (missing or + /// malformed env vars / config file). + public let credentialsValid: Bool + /// Whether the configuration carries API + web-auth tokens. User-identity + /// routes (`fetchCaller`, `lookupUsers*`) require this. + public let webAuthConfigured: Bool + /// Whether the configuration carries a key ID + private key material. + /// Required to sign `.public` database requests. + public let serverToServerConfigured: Bool + /// Caller info returned by `users/caller`. Nil when the network check was + /// skipped or when web-auth wasn't configured. + public let userInfo: UserInfo? + /// Zones returned by the optional `--test-query`. Nil when the flag wasn't + /// passed or the call failed (in which case `errors` carries the reason). + public let zonesFound: Int? + /// Human-readable error messages collected during validation. Empty on + /// full success. + public let errors: [String] + + /// Creates a new instance. + public init( + credentialsValid: Bool, + webAuthConfigured: Bool, + serverToServerConfigured: Bool, + userInfo: UserInfo? = nil, + zonesFound: Int? = nil, + errors: [String] = [] + ) { + self.credentialsValid = credentialsValid + self.webAuthConfigured = webAuthConfigured + self.serverToServerConfigured = serverToServerConfigured + self.userInfo = userInfo + self.zonesFound = zonesFound + self.errors = errors + } +} diff --git a/Examples/MistDemo/Tests/MistDemoTests/Commands/DiscoverCommandTests.swift b/Examples/MistDemo/Tests/MistDemoTests/Commands/DiscoverCommandTests.swift new file mode 100644 index 00000000..387d5264 --- /dev/null +++ b/Examples/MistDemo/Tests/MistDemoTests/Commands/DiscoverCommandTests.swift @@ -0,0 +1,84 @@ +// +// DiscoverCommandTests.swift +// MistDemoTests +// +// Created by Leo Dion. +// Copyright © 2026 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +internal import Foundation +internal import Testing + +@testable import MistDemoKit + +@Suite("DiscoverCommand Tests") +internal struct DiscoverCommandTests { + @Test("Command has correct static properties") + internal func staticProperties() { + #expect(DiscoverCommand.commandName == "discover") + #expect(DiscoverCommand.abstract == "Discover user identities by email") + #expect(DiscoverCommand.helpText.contains("DISCOVER")) + } + + @Test("Config initializes with emails") + internal func configWithEmails() async throws { + let baseConfig = try await MistDemoConfig() + let config = DiscoverConfig( + base: baseConfig, + emails: ["alice@example.com", "bob@example.com"] + ) + #expect(config.emails.count == 2) + #expect(config.output == .json) + } + + @Test("Execute rejects empty emails") + internal func rejectsEmptyEmails() async throws { + let baseConfig = try await MistDemoConfig() + let config = DiscoverConfig(base: baseConfig, emails: []) + let command = DiscoverCommand(config: config) + + await #expect(throws: DiscoverError.self) { + try await command.execute() + } + } + + @Test("Execute rejects missing web-auth") + internal func rejectsMissingWebAuth() async throws { + // Construct a config that explicitly lacks web-auth so the test does + // not depend on whether a local .env happens to provide credentials. + let baseConfig = try await MistDemoConfig( + apiToken: "test-api-token", + webAuthToken: nil + ) + let config = DiscoverConfig( + base: baseConfig, + emails: ["alice@example.com"] + ) + let command = DiscoverCommand(config: config) + + await #expect(throws: DiscoverError.self) { + try await command.execute() + } + } +} diff --git a/Examples/MistDemo/Tests/MistDemoTests/Commands/ListZonesCommandTests.swift b/Examples/MistDemo/Tests/MistDemoTests/Commands/ListZonesCommandTests.swift new file mode 100644 index 00000000..697d1a64 --- /dev/null +++ b/Examples/MistDemo/Tests/MistDemoTests/Commands/ListZonesCommandTests.swift @@ -0,0 +1,77 @@ +// +// ListZonesCommandTests.swift +// MistDemoTests +// +// Created by Leo Dion. +// Copyright © 2026 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +internal import Foundation +internal import MistKit +internal import Testing + +@testable import MistDemoKit + +@Suite("ListZonesCommand Tests") +internal struct ListZonesCommandTests { + @Test("Command has correct static properties") + internal func staticProperties() { + #expect(ListZonesCommand.commandName == "list-zones") + #expect(ListZonesCommand.abstract == "List all zones in the database") + #expect(ListZonesCommand.helpText.contains("LIST-ZONES")) + } + + @Test("Config defaults") + internal func configDefaults() async throws { + let baseConfig = try await MistDemoConfig() + let config = ListZonesConfig(base: baseConfig) + #expect(config.includeDefault == false) + #expect(config.output == .table) + } + + @Test("Config accepts custom values") + internal func configCustom() async throws { + let baseConfig = try await MistDemoConfig() + let config = ListZonesConfig( + base: baseConfig, + includeDefault: true, + output: .json + ) + #expect(config.includeDefault) + #expect(config.output == .json) + } + + @Test("Execute rejects public database") + internal func rejectsPublicDatabase() async throws { + let baseConfig = try await MistDemoConfig( + database: .public(.prefers(.serverToServer)) + ) + let config = ListZonesConfig(base: baseConfig) + let command = ListZonesCommand(config: config) + + await #expect(throws: ListZonesError.self) { + try await command.execute() + } + } +} diff --git a/Examples/MistDemo/Tests/MistDemoTests/Commands/ModifyZonesCommandTests.swift b/Examples/MistDemo/Tests/MistDemoTests/Commands/ModifyZonesCommandTests.swift new file mode 100644 index 00000000..e1419809 --- /dev/null +++ b/Examples/MistDemo/Tests/MistDemoTests/Commands/ModifyZonesCommandTests.swift @@ -0,0 +1,135 @@ +// +// ModifyZonesCommandTests.swift +// MistDemoTests +// +// Created by Leo Dion. +// Copyright © 2026 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +internal import Foundation +internal import MistKit +internal import Testing + +@testable import MistDemoKit + +@Suite("ModifyZonesCommand Tests") +internal struct ModifyZonesCommandTests { + @Test("Command has correct static properties") + internal func staticProperties() { + #expect(ModifyZonesCommand.commandName == "modify-zones") + #expect(ModifyZonesCommand.abstract == "Create or delete CloudKit zones") + #expect(ModifyZonesCommand.helpText.contains("MODIFY-ZONES")) + } + + @Test("Config initializes with operations") + internal func configWithOperations() async throws { + let baseConfig = try await MistDemoConfig() + let ops = [ + ZoneOperationInput(type: "create", zoneName: "Articles"), + ZoneOperationInput(type: "delete", zoneName: "Archive"), + ] + let config = ModifyZonesConfig(base: baseConfig, operations: ops) + #expect(config.operations.count == 2) + #expect(config.output == .json) + } + + @Test("Parse envelope with create + delete") + internal func parseEnvelopeMixed() throws { + let json = """ + { + "operations": [ + { "type": "create", "zoneName": "Articles" }, + { "type": "delete", "zoneName": "Archive" } + ] + } + """ + let data = Data(json.utf8) + let ops = try ModifyZonesConfig.parseOperations(from: data) + #expect(ops.count == 2) + #expect(ops[0] == ZoneOperationInput(type: "create", zoneName: "Articles")) + #expect(ops[1] == ZoneOperationInput(type: "delete", zoneName: "Archive")) + } + + @Test("Parse invalid JSON throws parsingFailed") + internal func parseInvalidJSONThrows() throws { + let data = Data("{ not json".utf8) + #expect(throws: ModifyZonesError.self) { + _ = try ModifyZonesConfig.parseOperations(from: data) + } + } + + @Test("ZoneOperationInput.create maps to .create") + internal func zoneOperationCreate() throws { + let input = ZoneOperationInput(type: "create", zoneName: "Articles") + let operation = try input.toZoneOperation() + if case .create(let zoneID) = operation { + #expect(zoneID.zoneName == "Articles") + } else { + Issue.record("Expected .create, got \(operation)") + } + } + + @Test("ZoneOperationInput.delete maps to .delete") + internal func zoneOperationDelete() throws { + let input = ZoneOperationInput(type: "delete", zoneName: "Archive") + let operation = try input.toZoneOperation() + if case .delete(let zoneID) = operation { + #expect(zoneID.zoneName == "Archive") + } else { + Issue.record("Expected .delete, got \(operation)") + } + } + + @Test("ZoneOperationInput rejects unknown type") + internal func zoneOperationUnknownType() { + let input = ZoneOperationInput(type: "weirdtype", zoneName: "X") + #expect(throws: ModifyZonesError.self) { + _ = try input.toZoneOperation() + } + } + + @Test("ZoneOperationInput rejects empty zone name") + internal func zoneOperationEmptyName() { + let input = ZoneOperationInput(type: "create", zoneName: " ") + #expect(throws: ModifyZonesError.self) { + _ = try input.toZoneOperation() + } + } + + @Test("Execute rejects public database") + internal func rejectsPublicDatabase() async throws { + let baseConfig = try await MistDemoConfig( + database: .public(.prefers(.serverToServer)) + ) + let config = ModifyZonesConfig( + base: baseConfig, + operations: [ZoneOperationInput(type: "create", zoneName: "X")] + ) + let command = ModifyZonesCommand(config: config) + + await #expect(throws: ModifyZonesError.self) { + try await command.execute() + } + } +} diff --git a/Examples/MistDemo/Tests/MistDemoTests/Commands/ValidateCommandTests.swift b/Examples/MistDemo/Tests/MistDemoTests/Commands/ValidateCommandTests.swift new file mode 100644 index 00000000..437689db --- /dev/null +++ b/Examples/MistDemo/Tests/MistDemoTests/Commands/ValidateCommandTests.swift @@ -0,0 +1,93 @@ +// +// ValidateCommandTests.swift +// MistDemoTests +// +// Created by Leo Dion. +// Copyright © 2026 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +internal import Foundation +internal import Testing + +@testable import MistDemoKit + +@Suite("ValidateCommand Tests") +internal struct ValidateCommandTests { + @Test("Command has correct static properties") + internal func staticProperties() { + #expect(ValidateCommand.commandName == "validate") + #expect( + ValidateCommand.abstract == "Validate CloudKit credentials and reachability" + ) + #expect(ValidateCommand.helpText.contains("VALIDATE")) + } + + @Test("Command initializes with config") + internal func initializesWithConfig() async throws { + let baseConfig = try await MistDemoConfig() + let config = ValidateConfig(base: baseConfig) + _ = ValidateCommand(config: config) + } + + @Test("Config defaults") + internal func configDefaults() async throws { + let baseConfig = try await MistDemoConfig() + let config = ValidateConfig(base: baseConfig) + #expect(config.skipNetwork == false) + #expect(config.testQuery == false) + #expect(config.output == .json) + } + + @Test("Config accepts custom values") + internal func configCustom() async throws { + let baseConfig = try await MistDemoConfig() + let config = ValidateConfig( + base: baseConfig, + skipNetwork: true, + testQuery: true, + output: .table + ) + #expect(config.skipNetwork) + #expect(config.testQuery) + #expect(config.output == .table) + } + + @Test("ValidationResult encodes") + internal func validationResultEncodes() throws { + let result = ValidationResult( + credentialsValid: true, + webAuthConfigured: false, + serverToServerConfigured: true, + errors: [] + ) + let data = try JSONEncoder().encode(result) + let json = try #require( + JSONSerialization.jsonObject(with: data) as? [String: Any] + ) + #expect(json["credentialsValid"] as? Bool == true) + #expect(json["webAuthConfigured"] as? Bool == false) + #expect(json["serverToServerConfigured"] as? Bool == true) + #expect((json["errors"] as? [String])?.isEmpty == true) + } +} From 4ccdee220a5120826b1241512c8c6ec9b6850742 Mon Sep 17 00:00:00 2001 From: leogdion Date: Thu, 21 May 2026 15:15:12 -0400 Subject: [PATCH 11/35] =?UTF-8?q?Make=20response=E2=86=92domain=20conversi?= =?UTF-8?q?on=20failures=20loud;=20add=20RecordResult=20(#372)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/MistDemo.yml | 24 ++- .../.claude/implementation-patterns.md | 9 +- .../BushelCloud/.claude/s2s-auth-details.md | 30 +-- Examples/BushelCloud/CLAUDE.md | 2 +- .../BushelCloud.docc/CloudKitIntegration.md | 9 +- .../CloudKit/BushelCloudKitService.swift | 17 +- .../Utilities/MockRecordInfo.swift | 25 --- .../Protocols/CloudKitRecordOperating.swift | 15 +- .../Services/CelestraError.swift | 5 +- Examples/MistDemo/Package.swift | 1 - .../MistDemoKit/Commands/LookupCommand.swift | 9 +- .../MistDemoKit/Commands/ModifyCommand.swift | 34 +-- .../Commands/UploadAssetCommand.swift | 4 +- .../Phases/DiscoverUserIdentitiesPhase.swift | 2 +- .../Phases/LookupRecordsPhase.swift | 5 +- .../Phases/LookupUsersByEmailPhase.swift | 2 +- .../Phases/LookupUsersByRecordNamePhase.swift | 2 +- .../MistDemoTests/UserInfoTestExtension.swift | 9 +- .../EnvironmentDiagnosticTests.swift | 74 +++++++ .../Utilities/TestPlatform.swift | 12 +- .../CloudKitService/CloudKitError.swift | 24 ++- .../CloudKitService+Classification.swift | 4 +- .../CloudKitService+ErrorHandling.swift | 6 + .../CloudKitService+LookupOperations.swift | 6 +- .../CloudKitService+ModifyZones.swift | 11 +- .../CloudKitService+Operations.swift | 2 +- .../CloudKitService+RecordManaging.swift | 6 +- .../CloudKitService+SyncOperations.swift | 2 +- .../CloudKitService+UserOperations.swift | 2 +- .../CloudKitService+WriteOperations.swift | 23 +- .../CloudKitService+ZoneOperations.swift | 24 +-- .../Documentation.docc/WorkingWithRecords.md | 15 +- Sources/MistKit/Models/BatchSyncResult.swift | 39 ++-- .../Models/CloudKitErrorConvertible.swift | 42 ++++ .../Models/ConversionError+Reporting.swift | 71 ++++++ Sources/MistKit/Models/ConversionError.swift | 97 +++++++++ .../FieldValues/FieldValue+Codable.swift | 9 +- .../FieldValue+Components+List.swift | 137 ++++++++++++ .../FieldValues/FieldValue+Components.swift | 141 ++++-------- .../MistKit/Models/Queries/QueryResult.swift | 8 +- .../MistKit/Models/RecordChangesResult.swift | 8 +- Sources/MistKit/Models/RecordInfo.swift | 58 +++-- .../Models/RecordOperationFailure.swift | 168 +++++++++++++++ Sources/MistKit/Models/RecordResult.swift | 88 ++++++++ Sources/MistKit/Models/RecordTimestamp.swift | 14 +- .../MistKit/Models/Users/UserIdentity.swift | 14 +- Sources/MistKit/Models/Users/UserInfo.swift | 29 ++- .../MistKit/Models/Users/UserRecordName.swift | 68 ++++++ .../Models/Zones/ZoneChangesResult.swift | 18 +- Sources/MistKit/Models/Zones/ZoneInfo.swift | 29 +++ Sources/MistKitOpenAPI/Types.swift | 166 +++++++++++++- ....DiscoverUserIdentities+SuccessCases.swift | 8 +- ...eTests.FetchZoneChanges+SuccessCases.swift | 17 +- ...ests.LookupUsersByEmail+SuccessCases.swift | 2 +- ...LookupUsersByRecordName+SuccessCases.swift | 2 +- ...erviceTests.LookupZones+SuccessCases.swift | 22 +- .../Models/BatchSyncResultTests.swift | 64 +++--- .../Models/ConversionFailureTests.swift | 204 ++++++++++++++++++ .../MistKitTests/Models/RecordInfoTests.swift | 31 ++- .../Models/ServerErrorCodeTests.swift | 63 ++++++ openapi.yaml | 53 ++++- 61 files changed, 1695 insertions(+), 390 deletions(-) create mode 100644 Examples/MistDemo/Tests/MistDemoTests/Utilities/EnvironmentDiagnosticTests.swift create mode 100644 Sources/MistKit/Models/CloudKitErrorConvertible.swift create mode 100644 Sources/MistKit/Models/ConversionError+Reporting.swift create mode 100644 Sources/MistKit/Models/ConversionError.swift create mode 100644 Sources/MistKit/Models/FieldValues/FieldValue+Components+List.swift create mode 100644 Sources/MistKit/Models/RecordOperationFailure.swift create mode 100644 Sources/MistKit/Models/RecordResult.swift create mode 100644 Sources/MistKit/Models/Users/UserRecordName.swift create mode 100644 Tests/MistKitTests/Models/ConversionFailureTests.swift create mode 100644 Tests/MistKitTests/Models/ServerErrorCodeTests.swift diff --git a/.github/workflows/MistDemo.yml b/.github/workflows/MistDemo.yml index 0c628db9..747f2378 100644 --- a/.github/workflows/MistDemo.yml +++ b/.github/workflows/MistDemo.yml @@ -201,6 +201,11 @@ jobs: name: Build on macOS runs-on: macos-26 if: ${{ !contains(github.event.head_commit.message, 'ci skip') }} + # Forward CI=true into the iOS simulator's test-runner process so + # `TestPlatform.isFlakyTimeoutSimulator` tolerates the cooperative-executor + # timeout flakes on the iOS sim. See the matching env on build-macos-platforms. + env: + TEST_RUNNER_CI: "true" strategy: fail-fast: false matrix: @@ -248,6 +253,17 @@ jobs: needs: configure runs-on: macos-26 if: ${{ needs.configure.outputs.full-matrix == 'true' && !contains(github.event.head_commit.message, 'ci skip') }} + # Forward CI=true into the simulator's test-runner process. `xcodebuild + # test` re-exports any host env var prefixed `TEST_RUNNER_` to the test + # runner with the prefix stripped, so `TEST_RUNNER_CI` arrives as `CI` + # inside the sim. An env dump (EnvironmentDiagnosticTests) confirmed the + # earlier `SIMCTL_CHILD_CI` approach never reached the runner — those vars + # only apply to `simctl spawn`/`launch`, not xctest — which is why the + # cooperative-executor flake gates in #334 still surfaced as real failures. + # With `CI` now visible, `TestPlatform.isFlakyTimeoutSimulator` tolerates + # the iOS/watchOS/visionOS sim flakes instead. + env: + TEST_RUNNER_CI: "true" strategy: fail-fast: false matrix: @@ -280,14 +296,6 @@ jobs: - uses: actions/checkout@v6 - name: Build and Test id: build - # Forward CI=true into the simulator's test process. xcodebuild launches - # simulator tests via simctl, which only propagates env vars prefixed - # with SIMCTL_CHILD_ (stripping the prefix on arrival). Without this, - # `TestPlatform.isFlakyTimeoutSimulator` reads ProcessInfo["CI"] as nil - # inside the sim and the cooperative-executor flake gates in #334 stay - # strict on visionOS/watchOS sim CI runs, surfacing as real failures. - env: - SIMCTL_CHILD_CI: "true" uses: brightdigit/swift-build@v1 with: scheme: ${{ env.PACKAGE_NAME }}-Package diff --git a/Examples/BushelCloud/.claude/implementation-patterns.md b/Examples/BushelCloud/.claude/implementation-patterns.md index 82085430..adaf2a2f 100644 --- a/Examples/BushelCloud/.claude/implementation-patterns.md +++ b/Examples/BushelCloud/.claude/implementation-patterns.md @@ -275,8 +275,8 @@ for (index, batch) in batches.enumerated() { // Process results immediately for result in results { - if result.recordType == "Unknown" { - // Handle error + if case .failure(let error) = result { + // Handle error (error.serverErrorCode, error.reason, ...) } } } @@ -313,11 +313,12 @@ try await syncXcodeVersions() // References uploaded records **Check for partial failures:** ```swift let results = try await service.modifyRecords(batch) -let errors = results.filter { $0.recordType == "Unknown" } +let errors = results.compactMap(\.error) if !errors.isEmpty { for error in errors { - print("Failed: \(error.recordName ?? "unknown")") + print("Failed: \(error.recordName)") + print("Code: \(error.serverErrorCode.rawValue)") print("Reason: \(error.reason ?? "N/A")") } } diff --git a/Examples/BushelCloud/.claude/s2s-auth-details.md b/Examples/BushelCloud/.claude/s2s-auth-details.md index ae3503f9..61b03f72 100644 --- a/Examples/BushelCloud/.claude/s2s-auth-details.md +++ b/Examples/BushelCloud/.claude/s2s-auth-details.md @@ -242,8 +242,8 @@ func syncRecords(_ records: [RestoreImageRecord]) async throws { let results = try await service.modifyRecords(batch) // Check for partial failures - let failures = results.filter { $0.recordType == "Unknown" } - let successes = results.filter { $0.recordType != "Unknown" } + let failures = results.compactMap(\.error) + let successes = results.compactMap(\.record) print("✓ \(successes.count) succeeded, ❌ \(failures.count) failed") } @@ -258,14 +258,13 @@ CloudKit returns **partial success** - some operations may succeed while others let results = try await service.modifyRecords(batch) for result in results { - if result.recordType == "Unknown" { - // This is an error response - print("❌ Error for \(result.recordName ?? "unknown")") - print(" Code: \(result.serverErrorCode ?? "N/A")") - print(" Reason: \(result.reason ?? "N/A")") - } else { - // Successfully created/updated - print("✓ Success: \(result.recordName ?? "unknown")") + switch result { + case .failure(let error): + print("❌ Error for \(error.recordName)") + print(" Code: \(error.serverErrorCode.rawValue)") + print(" Reason: \(error.reason ?? "N/A")") + case .success(let record): + print("✓ Success: \(record.recordName)") } } ``` @@ -333,10 +332,13 @@ let operation = RecordOperation.create( let results = try await service.modifyRecords([operation]) -if results.first?.recordType == "Unknown" { - print("❌ Failed: \(results.first?.reason ?? "unknown")") -} else { - print("✓ Success! Record created: \(results.first?.recordName ?? "")") +switch results.first { +case .failure(let error): + print("❌ Failed: \(error.reason ?? error.serverErrorCode.rawValue)") +case .success(let record): + print("✓ Success! Record created: \(record.recordName)") +case nil: + print("❌ No result returned") } ``` diff --git a/Examples/BushelCloud/CLAUDE.md b/Examples/BushelCloud/CLAUDE.md index 5c10c079..faa74ea4 100644 --- a/Examples/BushelCloud/CLAUDE.md +++ b/Examples/BushelCloud/CLAUDE.md @@ -452,7 +452,7 @@ See `.claude/s2s-auth-details.md` for detailed batch operation examples and erro - `BushelCloudKitError` enum defines project-specific errors - MistKit operations throw `CloudKitError` for API failures -- Use `RecordInfo.isError` to detect partial batch failures +- `modifyRecords` returns `[RecordResult]`; switch over each (`.success` / `.failure`) to detect partial batch failures - Verbose mode logs error details (serverErrorCode, reason) **Common error codes**: diff --git a/Examples/BushelCloud/Sources/BushelCloudKit/BushelCloud.docc/CloudKitIntegration.md b/Examples/BushelCloud/Sources/BushelCloudKit/BushelCloud.docc/CloudKitIntegration.md index 27bcc79c..28f933d3 100644 --- a/Examples/BushelCloud/Sources/BushelCloudKit/BushelCloud.docc/CloudKitIntegration.md +++ b/Examples/BushelCloud/Sources/BushelCloudKit/BushelCloud.docc/CloudKitIntegration.md @@ -125,8 +125,11 @@ let results = try await service.modifyRecords( database: .public(.prefers(.serverToServer)) ) for result in results { - if result.isError { - logger.error("Failed: \\(result.serverErrorCode ?? "unknown")") + switch result { + case .success(let record): + logger.debug("Saved: \\(record.recordName)") + case .failure(let error): + logger.error("Failed \\(error.recordName): \\(error.serverErrorCode.rawValue)") } } ``` @@ -155,6 +158,6 @@ This logs: 1. **Batch wisely**: Stay under 200 operations per request 2. **Order matters**: Upload dependencies first (SwiftVersion before XcodeVersion) -3. **Handle partials**: Check `RecordInfo.isError` for each result +3. **Handle partials**: Switch over each `RecordResult` (`.success` / `.failure`) 4. **Use references**: Link related records with CloudKit references 5. **Verbose development**: Use `--verbose` flag during development diff --git a/Examples/BushelCloud/Sources/BushelCloudKit/CloudKit/BushelCloudKitService.swift b/Examples/BushelCloud/Sources/BushelCloudKit/CloudKit/BushelCloudKitService.swift index 433f87d3..7c12482d 100644 --- a/Examples/BushelCloud/Sources/BushelCloudKit/CloudKit/BushelCloudKitService.swift +++ b/Examples/BushelCloud/Sources/BushelCloudKit/CloudKit/BushelCloudKitService.swift @@ -239,28 +239,29 @@ public struct BushelCloudKitService: Sendable, RecordManaging, CloudKitRecordCol ) Self.logger.debug( - "Received \(results.count) RecordInfo responses from CloudKit" + "Received \(results.count) per-record results from CloudKit" ) // Track results based on classification for result in results { - if result.isError { + switch result { + case .failure(let error): totalFailed += 1 - failedRecordNames.append(result.recordName) + failedRecordNames.append(error.recordName) Self.logger.debug( - "Error: recordName=\(result.recordName)" + "Error: recordName=\(error.recordName), code=\(error.serverErrorCode.rawValue)" ) - } else { + case .success(let record): // Classify as create or update based on pre-fetch - if classification.creates.contains(result.recordName) { + if classification.creates.contains(record.recordName) { totalCreated += 1 - } else if classification.updates.contains(result.recordName) { + } else if classification.updates.contains(record.recordName) { totalUpdated += 1 } } } - let batchSucceeded = results.filter { !$0.isError }.count + let batchSucceeded = results.filter { $0.record != nil }.count let batchFailed = results.count - batchSucceeded if batchFailed > 0 { diff --git a/Examples/BushelCloud/Tests/BushelCloudKitTests/Utilities/MockRecordInfo.swift b/Examples/BushelCloud/Tests/BushelCloudKitTests/Utilities/MockRecordInfo.swift index 03acc4c6..a7f6fc4b 100644 --- a/Examples/BushelCloud/Tests/BushelCloudKitTests/Utilities/MockRecordInfo.swift +++ b/Examples/BushelCloud/Tests/BushelCloudKitTests/Utilities/MockRecordInfo.swift @@ -27,7 +27,6 @@ // OTHER DEALINGS IN THE SOFTWARE. // -public import Foundation public import MistKit /// Helper to create RecordInfo from field dictionaries for testing roundtrips @@ -51,28 +50,4 @@ public enum MockRecordInfo: Sendable { fields: fields ) } - - /// Creates a RecordInfo with an error for testing error handling - /// - /// - Parameters: - /// - recordName: CloudKit record name - /// - errorCode: Server error code (stored in fields for test verification) - /// - reason: Error reason message (stored in fields for test verification) - /// - Returns: A RecordInfo marked as an error (isError == true) - public static func createError( - recordType _: String, - recordName: String, - errorCode: String, - reason: String - ) -> RecordInfo { - RecordInfo( - recordName: recordName, - recordType: "Unknown", // Marks this as an error (isError will be true) - recordChangeTag: nil, - fields: [ - "serverErrorCode": .string(errorCode), - "reason": .string(reason), - ] - ) - } } diff --git a/Examples/CelestraCloud/Sources/CelestraCloudKit/Protocols/CloudKitRecordOperating.swift b/Examples/CelestraCloud/Sources/CelestraCloudKit/Protocols/CloudKitRecordOperating.swift index 71f65aee..01afb30f 100644 --- a/Examples/CelestraCloud/Sources/CelestraCloudKit/Protocols/CloudKitRecordOperating.swift +++ b/Examples/CelestraCloud/Sources/CelestraCloudKit/Protocols/CloudKitRecordOperating.swift @@ -77,15 +77,26 @@ public protocol CloudKitRecordOperating: Sendable { // MARK: - CloudKitService Conformance extension CloudKitService: CloudKitRecordOperating { - /// Satisfy CloudKitRecordOperating protocol by forwarding to modifyRecords(_:atomic:) + /// Satisfy CloudKitRecordOperating protocol by forwarding to modifyRecords(_:atomic:). + /// + /// MistKit now returns `[RecordResult]`; this protocol keeps the `[RecordInfo]` contract, + /// so a per-record `.failure` is rethrown as `CloudKitError.recordOperationFailed`. That + /// matches Celestra's conservative all-or-nothing batch handling (a failure surfaces as a + /// thrown error, which the caller treats as a failed batch). public func modifyRecords(_ operations: [RecordOperation]) async throws(CloudKitError) -> [RecordInfo] { - try await modifyRecords( + let results = try await modifyRecords( operations, atomic: false, database: .public(.prefers(.serverToServer)) ) + var records: [RecordInfo] = [] + records.reserveCapacity(results.count) + for result in results { + records.append(try result.get()) + } + return records } /// Satisfy CloudKitRecordOperating's `queryRecords` (no database param) by forwarding to the public-database overload. diff --git a/Examples/CelestraCloud/Sources/CelestraCloudKit/Services/CelestraError.swift b/Examples/CelestraCloud/Sources/CelestraCloudKit/Services/CelestraError.swift index 72d34d0c..cdc6e089 100644 --- a/Examples/CelestraCloud/Sources/CelestraCloudKit/Services/CelestraError.swift +++ b/Examples/CelestraCloud/Sources/CelestraCloudKit/Services/CelestraError.swift @@ -144,9 +144,12 @@ public enum CelestraError: LocalizedError { case .decodingError: // Decoding errors are not retriable (data format issue) return false - case .unsupportedOperationType, .paginationLimitExceeded: + case .unsupportedOperationType, .paginationLimitExceeded, .zonePaginationLimitExceeded: // Programmer/configuration issues — not retriable return false + case .conversionFailed, .recordOperationFailed: + // Response could not be mapped, or a per-record operation failed — not retriable + return false case .missingCredentials, .invalidPrivateKey: // Credential/configuration issues — not retriable return false diff --git a/Examples/MistDemo/Package.swift b/Examples/MistDemo/Package.swift index f2e35720..9a488def 100644 --- a/Examples/MistDemo/Package.swift +++ b/Examples/MistDemo/Package.swift @@ -174,7 +174,6 @@ let package = Package( "MistDemoKit", .product(name: "ConfigKeyKit", package: "ConfigKeyKit"), .product(name: "MistKit", package: "MistKit"), - .product(name: "MistKitOpenAPI", package: "MistKit"), .product( name: "Hummingbird", package: "hummingbird", diff --git a/Examples/MistDemo/Sources/MistDemoKit/Commands/LookupCommand.swift b/Examples/MistDemo/Sources/MistDemoKit/Commands/LookupCommand.swift index 3e70983f..b291301b 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Commands/LookupCommand.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Commands/LookupCommand.swift @@ -76,14 +76,19 @@ public struct LookupCommand: MistDemoCommand, OutputFormatting { do { let client = try MistKitClientFactory.create(for: config.base) - let records = try await client.lookupRecords( + let results = try await client.lookupRecords( recordNames: config.recordNames, desiredKeys: config.fields, database: config.base.database ) + // A per-record lookup failure (e.g. NOT_FOUND) comes back as `.failure`. + let records = results.compactMap { result in + if case .success(let record) = result { record } else { nil } + } + // Report missing names to stderr so a JSON/CSV/etc. stdout stream stays parseable - let foundNames = Set(records.compactMap { $0.recordName }) + let foundNames = Set(records.map(\.recordName)) let missing = config.recordNames.filter { !foundNames.contains($0) } if !missing.isEmpty { let line = diff --git a/Examples/MistDemo/Sources/MistDemoKit/Commands/ModifyCommand.swift b/Examples/MistDemo/Sources/MistDemoKit/Commands/ModifyCommand.swift index 0f37e6d4..ab7a91d6 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Commands/ModifyCommand.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Commands/ModifyCommand.swift @@ -85,7 +85,14 @@ public struct ModifyCommand: MistDemoCommand, OutputFormatting { database: config.base.database ) - let rows = results.map { record in + let succeeded = results.compactMap { result in + if case .success(let record) = result { record } else { nil } + } + let failures = results.compactMap { result in + if case .failure(let error) = result { error } else { nil } + } + + let rows = succeeded.map { record in ModifyResultRow( operation: "applied", recordType: record.recordType, @@ -94,25 +101,22 @@ public struct ModifyCommand: MistDemoCommand, OutputFormatting { ) } - let recordReturningOpsCount = - config.operations - .filter { $0.operation != .delete }.count - let partialFailure = - !config.atomic - && results.count < recordReturningOpsCount - - if partialFailure { - let missing = recordReturningOpsCount - results.count - let line = - "Warning: \(missing) of \(recordReturningOpsCount)" - + " create/update op(s) did not return a record.\n" - FileHandle.standardError.write(Data(line.utf8)) + let partialFailure = !config.atomic && !failures.isEmpty + + if !failures.isEmpty { + for failure in failures { + let line = + "Warning: operation on '\(failure.recordName)' failed" + + " (\(failure.serverErrorCode.rawValue))" + + (failure.reason.map { ": \($0)" } ?? "") + "\n" + FileHandle.standardError.write(Data(line.utf8)) + } } let envelope = ModifyOutput( results: rows, attempted: config.operations.count, - succeeded: results.count, + succeeded: succeeded.count, partialFailure: partialFailure ) try await outputResult(envelope, format: config.output) diff --git a/Examples/MistDemo/Sources/MistDemoKit/Commands/UploadAssetCommand.swift b/Examples/MistDemo/Sources/MistDemoKit/Commands/UploadAssetCommand.swift index 50ad9d49..8d87a9aa 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Commands/UploadAssetCommand.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Commands/UploadAssetCommand.swift @@ -202,7 +202,9 @@ public struct UploadAssetCommand: MistDemoCommand, OutputFormatting { recordNames: [recordName], database: config.base.database ) - guard let existingRecord = existingRecords.first else { + guard let firstResult = existingRecords.first, + case .success(let existingRecord) = firstResult + else { throw UploadAssetError.operationFailed( "Record '\(recordName)' not found" ) diff --git a/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/DiscoverUserIdentitiesPhase.swift b/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/DiscoverUserIdentitiesPhase.swift index ac7e3ae1..cd7ca11c 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/DiscoverUserIdentitiesPhase.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/DiscoverUserIdentitiesPhase.swift @@ -57,7 +57,7 @@ internal struct DiscoverUserIdentitiesPhase: IntegrationPhase { if context.verbose { for identity in identities { - if let name = identity.userRecordName { print(" - \(name)") } + if case .recordName(let name) = identity.userRecordName { print(" - \(name)") } } } diff --git a/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/LookupRecordsPhase.swift b/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/LookupRecordsPhase.swift index 803ef055..23c0e6e4 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/LookupRecordsPhase.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/LookupRecordsPhase.swift @@ -48,10 +48,13 @@ internal struct LookupRecordsPhase: IntegrationPhase { print(" Looking up \(lookupNames.count) of \(input.names.count) record(s) by name") } - let records = try await context.service.lookupRecords( + let results = try await context.service.lookupRecords( recordNames: lookupNames, database: context.database ) + let records = results.compactMap { result in + if case .success(let record) = result { record } else { nil } + } print("✅ Looked up \(records.count) record(s)") diff --git a/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/LookupUsersByEmailPhase.swift b/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/LookupUsersByEmailPhase.swift index 326ed718..858eafb0 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/LookupUsersByEmailPhase.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/LookupUsersByEmailPhase.swift @@ -78,7 +78,7 @@ internal struct LookupUsersByEmailPhase: IntegrationPhase { if context.verbose { for identity in identities { - if let name = identity.userRecordName { print(" - \(name)") } + if case .recordName(let name) = identity.userRecordName { print(" - \(name)") } } } diff --git a/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/LookupUsersByRecordNamePhase.swift b/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/LookupUsersByRecordNamePhase.swift index 2e5a963f..6c5b58c6 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/LookupUsersByRecordNamePhase.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/LookupUsersByRecordNamePhase.swift @@ -55,7 +55,7 @@ internal struct LookupUsersByRecordNamePhase: IntegrationPhase { if context.verbose { for identity in identities { - if let name = identity.userRecordName { print(" - \(name)") } + if case .recordName(let name) = identity.userRecordName { print(" - \(name)") } } } diff --git a/Examples/MistDemo/Tests/MistDemoTests/UserInfoTestExtension.swift b/Examples/MistDemo/Tests/MistDemoTests/UserInfoTestExtension.swift index 7a2e388e..2a3777c3 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/UserInfoTestExtension.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/UserInfoTestExtension.swift @@ -27,9 +27,6 @@ // OTHER DEALINGS IN THE SOFTWARE. // -internal import Foundation -internal import MistKitOpenAPI - @testable import MistKit /// Test extension for UserInfo to enable test instance creation @@ -51,15 +48,11 @@ extension UserInfo { lastName: String? = nil, emailAddress: String? = nil ) -> UserInfo { - // Create a mock UserResponse to initialize UserInfo - // Using @testable import allows access to internal types - let userResponse = Components.Schemas.UserResponse( + UserInfo( userRecordName: userRecordName, firstName: firstName, lastName: lastName, emailAddress: emailAddress ) - - return UserInfo(from: userResponse) } } diff --git a/Examples/MistDemo/Tests/MistDemoTests/Utilities/EnvironmentDiagnosticTests.swift b/Examples/MistDemo/Tests/MistDemoTests/Utilities/EnvironmentDiagnosticTests.swift new file mode 100644 index 00000000..5e55c779 --- /dev/null +++ b/Examples/MistDemo/Tests/MistDemoTests/Utilities/EnvironmentDiagnosticTests.swift @@ -0,0 +1,74 @@ +// +// EnvironmentDiagnosticTests.swift +// MistDemo +// +// Created by Leo Dion. +// Copyright © 2026 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +internal import Foundation +internal import Testing + +/// Diagnostic suite that dumps the environment visible to the *test process*. +/// +/// Simulator test processes don't inherit the runner's shell environment, so +/// CI-detection flags like `CI` / `GITHUB_ACTIONS` may not survive into the +/// process that evaluates `TestPlatform.isFlakyTimeoutSimulator`. This test +/// surfaces the relevant variables so CI logs reveal exactly which CI markers — +/// if any — are actually present inside iOS / watchOS / visionOS simulators. +/// +/// `print()` from inside the simulator test process is dropped by xcodebuild's +/// console capture, so the dump is routed through Swift Testing's issue +/// reporting (which *is* captured) via `Issue.record` inside `withKnownIssue` — +/// the comment lands in the log while the recorded issue stays "known" so the +/// build stays green. Grep the job log for `ENV_DUMP:`. +@Suite("Environment Diagnostic") +internal struct EnvironmentDiagnosticTests { + @Test("Dump CI-relevant environment variables visible to the test process") + internal func dumpEnvironment() { + let environment = ProcessInfo.processInfo.environment + + // CI markers we'd consider keying the flake gate off of — print their + // values explicitly (these aren't secrets). Everything else is reported by + // name only so we don't leak token/key values. + let markerNames = [ + "CI", "GITHUB_ACTIONS", "GITHUB_RUN_ID", "GITHUB_WORKFLOW", + "RUNNER_OS", "TERM", "SIMULATOR_UDID", + ] + let markers = + markerNames + .map { "\($0)=\(environment[$0] ?? "")" } + .joined(separator: " ") + let allNames = environment.keys.sorted().joined(separator: ",") + + withKnownIssue("ENV_DUMP diagnostic (always recorded; surfaces env in CI)") { + Issue.record( + Comment( + rawValue: "ENV_DUMP: count=\(environment.count) | markers: \(markers)" + + " | names=[\(allNames)]" + ) + ) + } + } +} diff --git a/Examples/MistDemo/Tests/MistDemoTests/Utilities/TestPlatform.swift b/Examples/MistDemo/Tests/MistDemoTests/Utilities/TestPlatform.swift index 209dc43d..63ab5775 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Utilities/TestPlatform.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Utilities/TestPlatform.swift @@ -46,10 +46,16 @@ internal enum TestPlatform { /// True when running inside CI on a simulator whose cooperative executor can /// starve the polling timeout task in `withTimeout` — see #334. The race is - /// bounded to visionOS / watchOS simulators under CI load; local sim runs - /// (CI env unset) stay strict to surface real regressions in the helper. + /// bounded to iOS / visionOS / watchOS simulators under CI load; local sim + /// runs (CI env unset) stay strict to surface real regressions in the helper. + /// + /// The `CI` flag is delivered into the simulator's test-runner process via the + /// `TEST_RUNNER_CI` job-level env in the workflow — `xcodebuild test` + /// re-exports `TEST_RUNNER_`-prefixed host vars to the runner with the prefix + /// stripped, so it arrives as `CI`. (The earlier `SIMCTL_CHILD_CI` approach + /// never reached the xctest runner; an env dump confirmed it.) internal static let isFlakyTimeoutSimulator: Bool = { - #if (os(visionOS) || os(watchOS)) && targetEnvironment(simulator) + #if (os(iOS) || os(visionOS) || os(watchOS)) && targetEnvironment(simulator) return ProcessInfo.processInfo.environment["CI"] != nil #else return false diff --git a/Sources/MistKit/CloudKitService/CloudKitError.swift b/Sources/MistKit/CloudKitService/CloudKitError.swift index e13578e9..edc3a8b3 100644 --- a/Sources/MistKit/CloudKitService/CloudKitError.swift +++ b/Sources/MistKit/CloudKitService/CloudKitError.swift @@ -53,6 +53,16 @@ public enum CloudKitError: LocalizedError, Sendable { /// rolled back because at least one operation in the batch failed. case atomicFailure(reason: String?) case invalidResponse + /// A CloudKit response decoded at the transport layer but a specific value + /// could not be mapped into a MistKit domain type — e.g. an unmappable field + /// value, a record/zone/user missing a required identifier, or an unknown + /// union case. Wraps the structured ``ConversionError`` naming exactly what + /// failed (the field/zone/record and why). + case conversionFailed(ConversionError) + /// A per-record operation in a `modifyRecords`/`lookupRecords` batch came + /// back as a `RecordOperationFailure`, surfaced by a single-record + /// convenience (`createRecord`/`updateRecord`/`deleteRecord`). + case recordOperationFailed(RecordOperationFailure) case underlyingError(any Error) case decodingError(DecodingError) case networkError(URLError) @@ -80,7 +90,8 @@ public enum CloudKitError: LocalizedError, Sendable { return 413 case .badRequest, .atomicFailure: return 400 - case .invalidResponse, .underlyingError, .decodingError, .networkError, + case .invalidResponse, .conversionFailed, .recordOperationFailed, + .underlyingError, .decodingError, .networkError, .unsupportedOperationType, .paginationLimitExceeded, .zonePaginationLimitExceeded, .missingCredentials, .invalidPrivateKey: return nil @@ -105,6 +116,17 @@ public enum CloudKitError: LocalizedError, Sendable { return "CloudKit API error: HTTP \(statusCode)\nRaw Response: \(rawResponse)" case .invalidResponse: return "Invalid response from CloudKit" + case .conversionFailed(let conversionError): + return "Failed to convert CloudKit response into a MistKit type: " + + (conversionError.errorDescription ?? "\(conversionError)") + case .recordOperationFailed(let recordError): + var message = + "CloudKit record operation failed for '\(recordError.recordName)' " + + "(\(recordError.serverErrorCode.rawValue))" + if let reason = recordError.reason { + message += "\nReason: \(reason)" + } + return message case .underlyingError(let error): return "CloudKit operation failed with underlying error: \(String(reflecting: error))" case .decodingError(let error): diff --git a/Sources/MistKit/CloudKitService/CloudKitService+Classification.swift b/Sources/MistKit/CloudKitService/CloudKitService+Classification.swift index bab81204..f42ffe18 100644 --- a/Sources/MistKit/CloudKitService/CloudKitService+Classification.swift +++ b/Sources/MistKit/CloudKitService/CloudKitService+Classification.swift @@ -114,11 +114,11 @@ extension CloudKitService { atomic: Bool = false, database: Database ) async throws(CloudKitError) -> BatchSyncResult { - let records = try await modifyRecords( + let results = try await modifyRecords( operations, atomic: atomic, database: database ) - return BatchSyncResult(records: records, classification: classification) + return BatchSyncResult(results: results, classification: classification) } } diff --git a/Sources/MistKit/CloudKitService/CloudKitService+ErrorHandling.swift b/Sources/MistKit/CloudKitService/CloudKitService+ErrorHandling.swift index 4aa21477..2c3e3ae2 100644 --- a/Sources/MistKit/CloudKitService/CloudKitService+ErrorHandling.swift +++ b/Sources/MistKit/CloudKitService/CloudKitService+ErrorHandling.swift @@ -51,6 +51,12 @@ extension CloudKitService { return cloudKitError } + // A response→domain conversion (or other convertible error) failed; + // preserve the structured cause via its own mapping. + if let convertible = error as? any CloudKitErrorConvertible { + return convertible.asCloudKitError + } + // OpenAPIRuntime wraps transport-level errors in ClientError; unwrap to inspect the cause. let inspected: any Error = (error as? ClientError)?.underlyingError ?? error diff --git a/Sources/MistKit/CloudKitService/CloudKitService+LookupOperations.swift b/Sources/MistKit/CloudKitService/CloudKitService+LookupOperations.swift index a21e5568..91c14df8 100644 --- a/Sources/MistKit/CloudKitService/CloudKitService+LookupOperations.swift +++ b/Sources/MistKit/CloudKitService/CloudKitService+LookupOperations.swift @@ -50,11 +50,13 @@ extension CloudKitService { /// /// - Note: Pass `desiredKeys` to limit which fields come back. Useful /// for list views that only need a projection. + /// - Returns: A ``RecordResult`` per requested record — `.success` for a found + /// record, `.failure` (e.g. `NOT_FOUND`) for one CloudKit could not return. public func lookupRecords( recordNames: [String], desiredKeys: [String]? = nil, database: Database - ) async throws(CloudKitError) -> [RecordInfo] { + ) async throws(CloudKitError) -> [RecordResult] { do { let client = try self.client(for: database) let response = try await client.lookupRecords( @@ -79,7 +81,7 @@ extension CloudKitService { let lookupData: Components.Schemas.LookupResponse = try await responseProcessor.processLookupRecordsResponse(response) - return lookupData.records?.compactMap { RecordInfo(from: $0) } ?? [] + return try (lookupData.records ?? []).map { try RecordResult(from: $0) } } catch { throw mapToCloudKitError(error, context: "lookupRecords") } diff --git a/Sources/MistKit/CloudKitService/CloudKitService+ModifyZones.swift b/Sources/MistKit/CloudKitService/CloudKitService+ModifyZones.swift index 11dd57a5..d42fb743 100644 --- a/Sources/MistKit/CloudKitService/CloudKitService+ModifyZones.swift +++ b/Sources/MistKit/CloudKitService/CloudKitService+ModifyZones.swift @@ -87,16 +87,7 @@ extension CloudKitService { let zonesData: Components.Schemas.ZonesModifyResponse = try await responseProcessor.processModifyZonesResponse(response) - return zonesData.zones?.compactMap { zone in - guard let zoneID = zone.zoneID else { - return nil - } - return ZoneInfo( - zoneName: zoneID.zoneName ?? "Unknown", - ownerRecordName: zoneID.ownerName, - capabilities: [] - ) - } ?? [] + return try (zonesData.zones ?? []).map { try ZoneInfo(fromZoneID: $0.zoneID) } } catch { throw mapToCloudKitError(error, context: "modifyZones") } diff --git a/Sources/MistKit/CloudKitService/CloudKitService+Operations.swift b/Sources/MistKit/CloudKitService/CloudKitService+Operations.swift index 0faea7e0..4660d6ce 100644 --- a/Sources/MistKit/CloudKitService/CloudKitService+Operations.swift +++ b/Sources/MistKit/CloudKitService/CloudKitService+Operations.swift @@ -189,7 +189,7 @@ extension CloudKitService { let recordsData: Components.Schemas.QueryResponse = try await responseProcessor.processQueryRecordsResponse(response) - return QueryResult(from: recordsData) + return try QueryResult(from: recordsData) } catch { throw mapToCloudKitError(error, context: "queryRecords") } diff --git a/Sources/MistKit/CloudKitService/CloudKitService+RecordManaging.swift b/Sources/MistKit/CloudKitService/CloudKitService+RecordManaging.swift index 61745820..64af5f13 100644 --- a/Sources/MistKit/CloudKitService/CloudKitService+RecordManaging.swift +++ b/Sources/MistKit/CloudKitService/CloudKitService+RecordManaging.swift @@ -60,10 +60,14 @@ extension CloudKitService: RecordManaging { /// Execute a batch of record operations via modify public func executeBatchOperations(_ operations: [RecordOperation]) async throws { - _ = try await self.modifyRecords( + let results = try await self.modifyRecords( operations, database: .public(.prefers(.serverToServer)) ) + for result in results { + // `get()` rethrows a per-record failure as `recordOperationFailed`. + _ = try result.get() + } } /// Query all records of a specific type, automatically paginating diff --git a/Sources/MistKit/CloudKitService/CloudKitService+SyncOperations.swift b/Sources/MistKit/CloudKitService/CloudKitService+SyncOperations.swift index ef6ecb26..d4790e24 100644 --- a/Sources/MistKit/CloudKitService/CloudKitService+SyncOperations.swift +++ b/Sources/MistKit/CloudKitService/CloudKitService+SyncOperations.swift @@ -110,7 +110,7 @@ extension CloudKitService { response ) - return RecordChangesResult(from: changesData) + return try RecordChangesResult(from: changesData) } catch { throw mapToCloudKitError(error, context: "fetchRecordChanges") } diff --git a/Sources/MistKit/CloudKitService/CloudKitService+UserOperations.swift b/Sources/MistKit/CloudKitService/CloudKitService+UserOperations.swift index ef57ee60..37941edd 100644 --- a/Sources/MistKit/CloudKitService/CloudKitService+UserOperations.swift +++ b/Sources/MistKit/CloudKitService/CloudKitService+UserOperations.swift @@ -63,7 +63,7 @@ extension CloudKitService { let userData: Components.Schemas.UserResponse = try await responseProcessor.processGetCallerResponse(response) - return UserInfo(from: userData) + return try UserInfo(from: userData) } catch { throw mapToCloudKitError(error, context: "fetchCaller") } diff --git a/Sources/MistKit/CloudKitService/CloudKitService+WriteOperations.swift b/Sources/MistKit/CloudKitService/CloudKitService+WriteOperations.swift index c6b9322b..764f1072 100644 --- a/Sources/MistKit/CloudKitService/CloudKitService+WriteOperations.swift +++ b/Sources/MistKit/CloudKitService/CloudKitService+WriteOperations.swift @@ -67,13 +67,15 @@ extension CloudKitService { /// - operations: Array of record operations to perform /// - atomic: When true, the entire batch fails if any single operation fails (default: false) /// - database: The CloudKit database scope to modify (`.public`, `.private`, `.shared`) - /// - Returns: Array of RecordInfo for the modified records + /// - Returns: A ``RecordResult`` per operation — `.success` for a saved/deleted + /// record, `.failure` (a ``RecordOperationFailure``) for one that CloudKit + /// rejected. /// - Throws: CloudKitError if the operation fails public func modifyRecords( _ operations: [RecordOperation], atomic: Bool = false, database: Database - ) async throws(CloudKitError) -> [RecordInfo] { + ) async throws(CloudKitError) -> [RecordResult] { let apiOperations: [Components.Schemas.RecordOperation] do { apiOperations = try operations.map { @@ -107,8 +109,7 @@ extension CloudKitService { let modifyResponse: Components.Schemas.ModifyResponse = try await responseProcessor.processModifyRecordsResponse(response) - return modifyResponse.records?.compactMap { RecordInfo(from: $0) } - ?? [] + return try (modifyResponse.records ?? []).map { try RecordResult(from: $0) } } catch let cloudKitError as CloudKitError { throw cloudKitError.addingQuotaHint( Self.recordSizeQuotaHint(for: apiOperations) @@ -148,10 +149,10 @@ extension CloudKitService { ) let results = try await modifyRecords([operation], database: database) - guard let record = results.first else { + guard let result = results.first else { throw CloudKitError.invalidResponse } - return record + return try result.get() } /// Update a single record in CloudKit @@ -189,10 +190,10 @@ extension CloudKitService { ) let results = try await modifyRecords([operation], database: database) - guard let record = results.first else { + guard let result = results.first else { throw CloudKitError.invalidResponse } - return record + return try result.get() } /// Delete a single record from CloudKit @@ -214,6 +215,10 @@ extension CloudKitService { recordChangeTag: recordChangeTag ) - _ = try await modifyRecords([operation], database: database) + let results = try await modifyRecords([operation], database: database) + for result in results { + // `get()` rethrows a per-record failure as `recordOperationFailed`. + _ = try result.get() + } } } diff --git a/Sources/MistKit/CloudKitService/CloudKitService+ZoneOperations.swift b/Sources/MistKit/CloudKitService/CloudKitService+ZoneOperations.swift index d423ffb4..9cd10449 100644 --- a/Sources/MistKit/CloudKitService/CloudKitService+ZoneOperations.swift +++ b/Sources/MistKit/CloudKitService/CloudKitService+ZoneOperations.swift @@ -62,16 +62,7 @@ extension CloudKitService { let zonesData: Components.Schemas.ZonesListResponse = try await responseProcessor.processListZonesResponse(response) - return zonesData.zones?.compactMap { zone in - guard let zoneID = zone.zoneID, let zoneName = zoneID.zoneName else { - return nil - } - return ZoneInfo( - zoneName: zoneName, - ownerRecordName: zoneID.ownerName, - capabilities: [] - ) - } ?? [] + return try (zonesData.zones ?? []).map { try ZoneInfo(fromZoneID: $0.zoneID) } } catch { throw mapToCloudKitError(error, context: "listZones") } @@ -122,16 +113,7 @@ extension CloudKitService { let zonesData: Components.Schemas.ZonesLookupResponse = try await responseProcessor.processLookupZonesResponse(response) - return zonesData.zones?.compactMap { zone in - guard let zoneID = zone.zoneID, let zoneName = zoneID.zoneName else { - return nil - } - return ZoneInfo( - zoneName: zoneName, - ownerRecordName: zoneID.ownerName, - capabilities: [] - ) - } ?? [] + return try (zonesData.zones ?? []).map { try ZoneInfo(fromZoneID: $0.zoneID) } } catch { throw mapToCloudKitError(error, context: "lookupZones") } @@ -185,7 +167,7 @@ extension CloudKitService { let changesData: Components.Schemas.ZoneChangesResponse = try await responseProcessor.processFetchZoneChangesResponse(response) - return ZoneChangesResult(from: changesData) + return try ZoneChangesResult(from: changesData) } catch { throw mapToCloudKitError(error, context: "fetchZoneChanges") } diff --git a/Sources/MistKit/Documentation.docc/WorkingWithRecords.md b/Sources/MistKit/Documentation.docc/WorkingWithRecords.md index 18ead4d6..1d728903 100644 --- a/Sources/MistKit/Documentation.docc/WorkingWithRecords.md +++ b/Sources/MistKit/Documentation.docc/WorkingWithRecords.md @@ -114,6 +114,17 @@ let results = try await service.modifyRecords( ) ``` +`modifyRecords` returns a ``RecordResult`` per operation — switch over each to handle per-record outcomes: + +```swift +for result in results { + switch result { + case .success(let record): print("saved \(record.recordName)") + case .failure(let error): print("failed \(error.recordName): \(error.serverErrorCode.rawValue)") + } +} +``` + | `atomic:` | Behavior | | --- | --- | | `false` (default) | Per-operation success/failure. Successful ops commit; failed ops surface in the response. | @@ -128,11 +139,13 @@ Choose `atomic: true` when the operations are semantically linked (paired update Use ``CloudKitService/lookupRecords(recordNames:desiredKeys:database:)`` to fetch known records by name: ```swift -let articles = try await service.lookupRecords( +let results = try await service.lookupRecords( recordNames: ["article-001", "article-002", "article-003"], desiredKeys: ["title", "publishedDate"], database: .private ) +// Each entry is a `RecordResult`; a not-found name comes back as `.failure`. +let articles = results.compactMap(\.record) ``` Pass `desiredKeys` to limit which fields come back — useful for list views that only need a subset. diff --git a/Sources/MistKit/Models/BatchSyncResult.swift b/Sources/MistKit/Models/BatchSyncResult.swift index b4d8ccdb..eef8bb2c 100644 --- a/Sources/MistKit/Models/BatchSyncResult.swift +++ b/Sources/MistKit/Models/BatchSyncResult.swift @@ -37,7 +37,7 @@ internal import Foundation /// /// - `created`: results whose record name was classified as a create /// - `updated`: results whose record name was classified as an update -/// - `failed`: results that came back as errors (`RecordInfo.isError == true`) +/// - `failed`: per-record errors (``RecordResult/failure(_:)``) returned by CloudKit /// - `unclassified`: successful results whose record name was in neither /// the creates nor updates sets — for example, anonymous creates where /// CloudKit assigned the record name server-side, or records whose name @@ -51,8 +51,8 @@ public struct BatchSyncResult: Sendable { /// Records classified as updates to existing records. public let updated: [RecordInfo] - /// Records that came back as errors. - public let failed: [RecordInfo] + /// Per-record errors returned by CloudKit for operations that failed. + public let failed: [RecordOperationFailure] /// Successful records that could not be classified as either a create or update. /// @@ -90,7 +90,7 @@ public struct BatchSyncResult: Sendable { internal init( created: [RecordInfo], updated: [RecordInfo], - failed: [RecordInfo], + failed: [RecordOperationFailure], unclassified: [RecordInfo] = [] ) { self.created = created @@ -102,8 +102,8 @@ public struct BatchSyncResult: Sendable { /// Partition a flat array of `RecordInfo` results into a `BatchSyncResult` /// using a pre-computed classification. /// - /// Each record is sorted as follows: - /// 1. If `record.isError` is `true`, it is added to `failed`. + /// Each result is sorted as follows: + /// 1. If the result is a `.failure`, its `RecordOperationFailure` is added to `failed`. /// 2. Else if `record.recordName` is in `classification.creates`, it is added /// to `created`. /// 3. Else if `record.recordName` is in `classification.updates`, it is added @@ -111,26 +111,29 @@ public struct BatchSyncResult: Sendable { /// 4. Otherwise it is added to `unclassified`. /// /// - Parameters: - /// - records: The records returned by `modifyRecords`. + /// - results: The per-record results returned by `modifyRecords`. /// - classification: The classification used to partition the records. internal init( - records: [RecordInfo], + results: [RecordResult], classification: OperationClassification ) { var created: [RecordInfo] = [] var updated: [RecordInfo] = [] - var failed: [RecordInfo] = [] + var failed: [RecordOperationFailure] = [] var unclassified: [RecordInfo] = [] - for record in records { - if record.isError { - failed.append(record) - } else if classification.creates.contains(record.recordName) { - created.append(record) - } else if classification.updates.contains(record.recordName) { - updated.append(record) - } else { - unclassified.append(record) + for result in results { + switch result { + case .failure(let error): + failed.append(error) + case .success(let record): + if classification.creates.contains(record.recordName) { + created.append(record) + } else if classification.updates.contains(record.recordName) { + updated.append(record) + } else { + unclassified.append(record) + } } } diff --git a/Sources/MistKit/Models/CloudKitErrorConvertible.swift b/Sources/MistKit/Models/CloudKitErrorConvertible.swift new file mode 100644 index 00000000..1605292c --- /dev/null +++ b/Sources/MistKit/Models/CloudKitErrorConvertible.swift @@ -0,0 +1,42 @@ +// +// CloudKitErrorConvertible.swift +// MistKit +// +// Created by Leo Dion. +// Copyright © 2026 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +/// An internal error type that knows how to represent itself as a +/// ``CloudKitError`` at the `CloudKitService` boundary (which throws +/// `CloudKitError`). +/// +/// `mapToCloudKitError(_:context:)` dispatches through this protocol so any +/// MistKit error carrying a structured cause — currently ``ConversionError`` — +/// is promoted without the boundary having to know each concrete type. +internal protocol CloudKitErrorConvertible: Error { + /// The ``CloudKitError`` this error maps to at the service boundary. + var asCloudKitError: CloudKitError { get } +} + +extension ConversionError: CloudKitErrorConvertible {} diff --git a/Sources/MistKit/Models/ConversionError+Reporting.swift b/Sources/MistKit/Models/ConversionError+Reporting.swift new file mode 100644 index 00000000..e6badb95 --- /dev/null +++ b/Sources/MistKit/Models/ConversionError+Reporting.swift @@ -0,0 +1,71 @@ +// +// ConversionError+Reporting.swift +// MistKit +// +// Created by Leo Dion. +// Copyright © 2026 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +internal import Logging + +/// Trap hook used by ``ConversionError/reportAndThrow(function:file:line:)``. +/// +/// The default handler calls Swift's `assertionFailure`, so a response→domain +/// conversion failure traps loudly in DEBUG. Tests override the handler via +/// `ConversionFailureReporter.$assertionHandler.withValue({ _, _, _ in }) { … }` +/// to exercise the throw path without trapping the test process. +internal enum ConversionFailureReporter { + @TaskLocal internal static var assertionHandler: @Sendable (String, StaticString, UInt) -> Void = + { message, file, line in + assertionFailure(message, file: file, line: line) + } +} + +extension ConversionError { + /// Wraps this typed conversion failure into a ``CloudKitError`` for the + /// `CloudKitService` boundary, which throws `CloudKitError`. + internal var asCloudKitError: CloudKitError { + .conversionFailed(self) + } + + /// Reports a response→domain conversion failure loudly and uniformly: + /// - logs at `.error` on every build (with full context), + /// - traps via the injectable assertion handler in DEBUG (no-op in release), + /// - always throws `self` (a typed ``ConversionError``) so callers never + /// receive silently-truncated data. + /// + /// The `-> Never` shape lets call sites read as a single `try` statement. + /// In DEBUG the handler traps *before* the throw is reached; in release it is + /// a no-op, so the function logs then throws. + internal func reportAndThrow( + function: StaticString = #function, + file: StaticString = #fileID, + line: UInt = #line + ) throws(ConversionError) -> Never { + let message = self.message + Logger(subsystem: .api).error("Conversion failure in \(function): \(message)") + ConversionFailureReporter.assertionHandler(message, file, line) + throw self + } +} diff --git a/Sources/MistKit/Models/ConversionError.swift b/Sources/MistKit/Models/ConversionError.swift new file mode 100644 index 00000000..832f2093 --- /dev/null +++ b/Sources/MistKit/Models/ConversionError.swift @@ -0,0 +1,97 @@ +// +// ConversionError.swift +// MistKit +// +// Created by Leo Dion. +// Copyright © 2026 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +public import Foundation + +/// A specific failure encountered while mapping a decoded CloudKit response +/// payload into a MistKit domain type. +/// +/// CloudKit responses decode at the transport layer but can still carry shapes +/// MistKit can't faithfully represent — an unmappable field value, a record or +/// zone missing a required identifier, a negative timestamp, and so on. Rather +/// than silently dropping or masking such data, the conversion initializers +/// throw a typed `ConversionError` naming exactly what failed. At the +/// `CloudKitService` boundary it is wrapped into +/// ``CloudKitError/conversionFailed(_:)``. +public enum ConversionError: LocalizedError, Sendable, Equatable { + /// A field value's structure matched no known `FieldValue` case. + case unmappableFieldValue(fieldName: String, value: String, type: String?) + /// A location field was missing its latitude and/or longitude. + case locationMissingCoordinates(fieldName: String) + /// A reference field was missing its `recordName`. + case referenceMissingRecordName(fieldName: String) + /// A list element matched no known `FieldValue` case. + case unmappableListItem(fieldName: String, item: String) + /// A nested-list element was not one of the supported basic types. + case unmappableNestedListItem(fieldName: String, item: String) + /// A record timestamp was negative. + case negativeTimestamp(milliseconds: Double) + /// A record response was missing `recordName` and/or `recordType`. + case recordMissingIdentifier(recordName: String?, recordType: String?) + /// A zone response was missing its `zoneID`. + case zoneMissingID + /// A zone response was missing its `zoneName`. + case zoneMissingName + /// A user response was missing its `userRecordName`. + case userMissingRecordName + + /// A human-readable description of what failed during conversion. + public var errorDescription: String? { + self.message + } + + /// The diagnostic message logged and trapped on by the conversion-failure + /// reporter, and surfaced via ``errorDescription``. + internal var message: String { + switch self { + case .unmappableFieldValue(let fieldName, let value, let type): + return "Unmappable FieldValue for field '\(fieldName)' " + + "(value: \(value), type: \(type ?? "nil"))" + case .locationMissingCoordinates(let fieldName): + return "Location field '\(fieldName)' missing latitude/longitude" + case .referenceMissingRecordName(let fieldName): + return "Reference field '\(fieldName)' missing recordName" + case .unmappableListItem(let fieldName, let item): + return "Unmappable list item for field '\(fieldName)' (\(item))" + case .unmappableNestedListItem(let fieldName, let item): + return "Unmappable nested list item for field '\(fieldName)' (\(item))" + case .negativeTimestamp(let milliseconds): + return "Invalid negative timestamp (\(milliseconds) ms)" + case .recordMissingIdentifier(let recordName, let recordType): + return "RecordResponse missing required identifier(s) " + + "(recordName: \(recordName ?? "nil"), recordType: \(recordType ?? "nil"))" + case .zoneMissingID: + return "Zone entry missing zoneID" + case .zoneMissingName: + return "Zone entry missing zoneName" + case .userMissingRecordName: + return "UserResponse missing userRecordName" + } + } +} diff --git a/Sources/MistKit/Models/FieldValues/FieldValue+Codable.swift b/Sources/MistKit/Models/FieldValues/FieldValue+Codable.swift index 24627740..bd02d094 100644 --- a/Sources/MistKit/Models/FieldValues/FieldValue+Codable.swift +++ b/Sources/MistKit/Models/FieldValues/FieldValue+Codable.swift @@ -86,10 +86,11 @@ extension FieldValue { if let value = try? container.decode(Asset.self) { return .asset(value) } - // Try to decode as date (milliseconds since epoch) - if let value = try? container.decode(Double.self) { - return .date(Date(timeIntervalSince1970: value / Self.millisecondsPerSecond)) - } + // No `.date` branch here on purpose: a CloudKit timestamp arrives as a bare + // millisecond `Double`, which `decodeBasicTypes` (run first) already claims + // as `.double`. A date fallback at this point was therefore unreachable — + // it never ran, so removing it changes no behavior. Encoding still emits + // `.date` values as milliseconds (see `encode(to:)`). return nil } diff --git a/Sources/MistKit/Models/FieldValues/FieldValue+Components+List.swift b/Sources/MistKit/Models/FieldValues/FieldValue+Components+List.swift new file mode 100644 index 00000000..8430ba36 --- /dev/null +++ b/Sources/MistKit/Models/FieldValues/FieldValue+Components+List.swift @@ -0,0 +1,137 @@ +// +// FieldValue+Components+List.swift +// MistKit +// +// Created by Leo Dion. +// Copyright © 2026 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +internal import Foundation +internal import MistKitOpenAPI + +/// List-value conversions for `FieldValue` ← `Components.Schemas` response types. +extension FieldValue { + /// Initialize from list field value + internal init( + listValue: [Components.Schemas.ListValuePayload], + fieldName: String + ) throws(ConversionError) { + var convertedList: [FieldValue] = [] + for item in listValue { + convertedList.append(try Self(listItem: item, fieldName: fieldName)) + } + self = .list(convertedList) + } + + /// Initialize from individual list item + internal init( + listItem: Components.Schemas.ListValuePayload, + fieldName: String + ) throws(ConversionError) { + if let simpleValue = Self.makeSimpleListItem(from: listItem) { + self = simpleValue + } else if let complexValue = try Self.makeComplexListItem(from: listItem, fieldName: fieldName) + { + self = complexValue + } else { + let failure = ConversionError.unmappableListItem(fieldName: fieldName, item: "\(listItem)") + try failure.reportAndThrow() + } + } + + /// Initialize from nested list value (simplified for basic types) + internal init( + nestedListValue: [Components.Schemas.ListValuePayload], + fieldName: String + ) throws(ConversionError) { + var convertedNestedList: [FieldValue] = [] + for item in nestedListValue { + convertedNestedList.append(try Self(basicListItem: item, fieldName: fieldName)) + } + self = .list(convertedNestedList) + } + + /// Initialize from basic list item types only + internal init( + basicListItem: Components.Schemas.ListValuePayload, + fieldName: String + ) throws(ConversionError) { + switch basicListItem { + case .StringValue(let stringValue): + self = .string(stringValue) + case .Int64Value(let intValue): + self = .int64(Int(intValue)) + case .DoubleValue(let doubleValue): + self = .double(doubleValue) + case .BytesValue(let bytesValue): + self = .bytes(bytesValue) + default: + let failure = ConversionError.unmappableNestedListItem( + fieldName: fieldName, + item: "\(basicListItem)" + ) + try failure.reportAndThrow() + } + } + + private static func makeSimpleListItem( + from listItem: Components.Schemas.ListValuePayload + ) -> FieldValue? { + if case .StringValue(let strVal) = listItem { + return .string(strVal) + } + if case .Int64Value(let intVal) = listItem { + return .int64(Int(intVal)) + } + if case .DoubleValue(let dblVal) = listItem { + return .double(dblVal) + } + if case .BytesValue(let bytesVal) = listItem { + return .bytes(bytesVal) + } + if case .DateValue(let dateVal) = listItem { + return .date(Date(timeIntervalSince1970: dateVal / 1_000)) + } + return nil + } + + private static func makeComplexListItem( + from listItem: Components.Schemas.ListValuePayload, + fieldName: String + ) throws(ConversionError) -> FieldValue? { + if case .LocationValue(let locationValue) = listItem { + return try Self(locationValue: locationValue, fieldName: fieldName) + } + if case .ReferenceValue(let referenceValue) = listItem { + return try Self(referenceValue: referenceValue, fieldName: fieldName) + } + if case .AssetValue(let assetValue) = listItem { + return Self(assetValue: assetValue) + } + if case .ListValue(let nestedList) = listItem { + return try Self(nestedListValue: nestedList, fieldName: fieldName) + } + return nil + } +} diff --git a/Sources/MistKit/Models/FieldValues/FieldValue+Components.swift b/Sources/MistKit/Models/FieldValues/FieldValue+Components.swift index 90298a29..f04858fd 100644 --- a/Sources/MistKit/Models/FieldValues/FieldValue+Components.swift +++ b/Sources/MistKit/Models/FieldValues/FieldValue+Components.swift @@ -32,31 +32,51 @@ internal import MistKitOpenAPI /// Extension to convert OpenAPI Components.Schemas.FieldValueResponse to MistKit FieldValue extension FieldValue { - /// Initialize from OpenAPI Components.Schemas.FieldValueResponse (from API responses) - internal init?(_ fieldData: Components.Schemas.FieldValueResponse) { - self.init(valuePayload: fieldData.value, typePayload: fieldData._type) + /// Initialize from OpenAPI Components.Schemas.FieldValueResponse (from API responses). + /// + /// - Parameters: + /// - fieldData: The decoded CloudKit field value to convert. + /// - fieldName: The name of the field being converted, used to give + /// conversion-failure diagnostics meaningful context. + /// - Throws: ``ConversionError`` if the value can't be mapped to a `FieldValue`. + internal init( + _ fieldData: Components.Schemas.FieldValueResponse, + fieldName: String + ) throws(ConversionError) { + try self.init(valuePayload: fieldData.value, typePayload: fieldData._type, fieldName: fieldName) } /// Initialize from field value and type - private init?( + private init( valuePayload: Components.Schemas.FieldValueResponse.valuePayload, - typePayload: Components.Schemas.FieldValueResponse._typePayload? - ) { + typePayload: Components.Schemas.FieldValueResponse._typePayload?, + fieldName: String + ) throws(ConversionError) { if let simpleValue = Self.makeSimpleFieldValue(from: valuePayload, type: typePayload) { self = simpleValue - } else if let complexValue = Self.makeComplexFieldValue(from: valuePayload) { + } else if let complexValue = + try Self.makeComplexFieldValue(from: valuePayload, fieldName: fieldName) + { self = complexValue } else { - return nil + let failure = ConversionError.unmappableFieldValue( + fieldName: fieldName, + value: "\(valuePayload)", + type: typePayload.map { "\($0)" } + ) + try failure.reportAndThrow() } } /// Initialize from location field value - private init?(locationValue: Components.Schemas.LocationValue) { + internal init( + locationValue: Components.Schemas.LocationValue, + fieldName: String + ) throws(ConversionError) { guard let latitude = locationValue.latitude, let longitude = locationValue.longitude else { - return nil + try ConversionError.locationMissingCoordinates(fieldName: fieldName).reportAndThrow() } let location = Location( @@ -73,7 +93,13 @@ extension FieldValue { } /// Initialize from reference field value - private init(referenceValue: Components.Schemas.ReferenceValue) { + internal init( + referenceValue: Components.Schemas.ReferenceValue, + fieldName: String + ) throws(ConversionError) { + guard let recordName = referenceValue.recordName else { + try ConversionError.referenceMissingRecordName(fieldName: fieldName).reportAndThrow() + } let action: Reference.Action? switch referenceValue.action { case .DELETE_SELF: @@ -84,14 +110,14 @@ extension FieldValue { action = nil } let reference = Reference( - recordName: referenceValue.recordName ?? "", + recordName: recordName, action: action ) self = .reference(reference) } /// Initialize from asset field value - private init(assetValue: Components.Schemas.AssetValue) { + internal init(assetValue: Components.Schemas.AssetValue) { let asset = Asset( fileChecksum: assetValue.fileChecksum, size: assetValue.size, @@ -103,45 +129,6 @@ extension FieldValue { self = .asset(asset) } - /// Initialize from list field value - private init(listValue: [Components.Schemas.ListValuePayload]) { - let convertedList = listValue.compactMap { Self(listItem: $0) } - self = .list(convertedList) - } - - /// Initialize from individual list item - private init?(listItem: Components.Schemas.ListValuePayload) { - if let simpleValue = Self.makeSimpleListItem(from: listItem) { - self = simpleValue - } else if let complexValue = Self.makeComplexListItem(from: listItem) { - self = complexValue - } else { - return nil - } - } - - /// Initialize from nested list value (simplified for basic types) - private init(nestedListValue: [Components.Schemas.ListValuePayload]) { - let convertedNestedList = nestedListValue.compactMap { Self(basicListItem: $0) } - self = .list(convertedNestedList) - } - - /// Initialize from basic list item types only - private init?(basicListItem: Components.Schemas.ListValuePayload) { - switch basicListItem { - case .StringValue(let stringValue): - self = .string(stringValue) - case .Int64Value(let intValue): - self = .int64(Int(intValue)) - case .DoubleValue(let doubleValue): - self = .double(doubleValue) - case .BytesValue(let bytesValue): - self = .bytes(bytesValue) - default: - return nil - } - } - private static func makeSimpleFieldValue( from value: Components.Schemas.FieldValueResponse.valuePayload, type fieldType: Components.Schemas.FieldValueResponse._typePayload? @@ -167,58 +154,20 @@ extension FieldValue { } private static func makeComplexFieldValue( - from value: Components.Schemas.FieldValueResponse.valuePayload - ) -> FieldValue? { + from value: Components.Schemas.FieldValueResponse.valuePayload, + fieldName: String + ) throws(ConversionError) -> FieldValue? { if case .LocationValue(let locationValue) = value { - return Self(locationValue: locationValue) + return try Self(locationValue: locationValue, fieldName: fieldName) } if case .ReferenceValue(let referenceValue) = value { - return Self(referenceValue: referenceValue) + return try Self(referenceValue: referenceValue, fieldName: fieldName) } if case .AssetValue(let assetValue) = value { return Self(assetValue: assetValue) } if case .ListValue(let listValue) = value { - return Self(listValue: listValue) - } - return nil - } - - private static func makeSimpleListItem( - from listItem: Components.Schemas.ListValuePayload - ) -> FieldValue? { - if case .StringValue(let strVal) = listItem { - return .string(strVal) - } - if case .Int64Value(let intVal) = listItem { - return .int64(Int(intVal)) - } - if case .DoubleValue(let dblVal) = listItem { - return .double(dblVal) - } - if case .BytesValue(let bytesVal) = listItem { - return .bytes(bytesVal) - } - if case .DateValue(let dateVal) = listItem { - return .date(Date(timeIntervalSince1970: dateVal / 1_000)) - } - return nil - } - - private static func makeComplexListItem( - from listItem: Components.Schemas.ListValuePayload - ) -> FieldValue? { - if case .LocationValue(let locationValue) = listItem { - return Self(locationValue: locationValue) - } - if case .ReferenceValue(let referenceValue) = listItem { - return Self(referenceValue: referenceValue) - } - if case .AssetValue(let assetValue) = listItem { - return Self(assetValue: assetValue) - } - if case .ListValue(let nestedList) = listItem { - return Self(nestedListValue: nestedList) + return try Self(listValue: listValue, fieldName: fieldName) } return nil } diff --git a/Sources/MistKit/Models/Queries/QueryResult.swift b/Sources/MistKit/Models/Queries/QueryResult.swift index d7c54f7f..6f016c82 100644 --- a/Sources/MistKit/Models/Queries/QueryResult.swift +++ b/Sources/MistKit/Models/Queries/QueryResult.swift @@ -48,8 +48,12 @@ public struct QueryResult: Codable, Sendable { self.continuationMarker = continuationMarker } - internal init(from response: Components.Schemas.QueryResponse) { - self.records = response.records?.compactMap { RecordInfo(from: $0) } ?? [] + internal init(from response: Components.Schemas.QueryResponse) throws(ConversionError) { + var records: [RecordInfo] = [] + for record in response.records ?? [] { + records.append(try RecordInfo(from: record)) + } + self.records = records self.continuationMarker = response.continuationMarker } } diff --git a/Sources/MistKit/Models/RecordChangesResult.swift b/Sources/MistKit/Models/RecordChangesResult.swift index 1296b0dc..b1e78caa 100644 --- a/Sources/MistKit/Models/RecordChangesResult.swift +++ b/Sources/MistKit/Models/RecordChangesResult.swift @@ -52,8 +52,12 @@ public struct RecordChangesResult: Codable, Sendable { self.moreComing = moreComing } - internal init(from response: Components.Schemas.ChangesResponse) { - self.records = response.records?.compactMap { RecordInfo(from: $0) } ?? [] + internal init(from response: Components.Schemas.ChangesResponse) throws(ConversionError) { + var records: [RecordInfo] = [] + for record in response.records ?? [] { + records.append(try RecordInfo(from: record)) + } + self.records = records self.syncToken = response.syncToken self.moreComing = response.moreComing ?? false } diff --git a/Sources/MistKit/Models/RecordInfo.swift b/Sources/MistKit/Models/RecordInfo.swift index aa5da904..7cfc6f3e 100644 --- a/Sources/MistKit/Models/RecordInfo.swift +++ b/Sources/MistKit/Models/RecordInfo.swift @@ -30,20 +30,14 @@ internal import Foundation internal import MistKitOpenAPI -/// Record information from CloudKit +/// Record information from CloudKit. /// -/// ## Error Detection Pattern -/// CloudKit Web Services returns error responses as records with nil `recordType` and `recordName`. -/// This implementation uses "Unknown" as a sentinel value for error responses, which can be detected -/// using the `isError` property. While this is a fragile pattern, it matches CloudKit's API behavior -/// where failed operations in a batch return incomplete record data. -/// -/// Example: -/// ```swift -/// let results = try await service.modifyRecords(operations) -/// let successfulRecords = results.filter { !$0.isError } -/// let failedRecords = results.filter { $0.isError } -/// ``` +/// A `RecordInfo` always represents a successfully-returned record: it carries a +/// non-optional `recordName` and `recordType`. Per-record failures from +/// `modifyRecords` / `lookupRecords` are surfaced separately as +/// ``RecordResult/failure(_:)`` (a ``RecordOperationFailure``) and never become a +/// `RecordInfo`. A response record missing its identifiers is treated as a +/// conversion failure (logged, asserted in DEBUG, and thrown). public struct RecordInfo: Codable, Sendable { /// The record name public let recordName: String @@ -61,21 +55,27 @@ public struct RecordInfo: Codable, Sendable { /// the record was deleted and should be removed from local storage. public let deleted: Bool - /// Indicates whether this RecordInfo represents an error response - /// - /// CloudKit returns error responses with nil recordType/recordName, which are converted - /// to "Unknown" during initialization. Use this property to detect failed operations - /// in batch modify responses. - public var isError: Bool { - recordType == "Unknown" - } - - internal init(from record: Components.Schemas.RecordResponse) { - self.recordName = record.recordName ?? "Unknown" - self.recordType = record.recordType ?? "Unknown" + internal init(from record: Components.Schemas.RecordResponse) throws(ConversionError) { + guard let recordName = record.recordName, let recordType = record.recordType else { + let failure = ConversionError.recordMissingIdentifier( + recordName: record.recordName, + recordType: record.recordType + ) + try failure.reportAndThrow() + } + self.recordName = recordName + self.recordType = recordType self.recordChangeTag = record.recordChangeTag - self.created = record.created.map(RecordTimestamp.init(from:)) - self.modified = record.modified.map(RecordTimestamp.init(from:)) + if let created = record.created { + self.created = try RecordTimestamp(from: created) + } else { + self.created = nil + } + if let modified = record.modified { + self.modified = try RecordTimestamp(from: modified) + } else { + self.modified = nil + } self.deleted = record.deleted ?? false // Convert fields to FieldValue representation @@ -83,9 +83,7 @@ public struct RecordInfo: Codable, Sendable { if let fieldsPayload = record.fields { for (fieldName, fieldData) in fieldsPayload.additionalProperties { - if let fieldValue = FieldValue(fieldData) { - convertedFields[fieldName] = fieldValue - } + convertedFields[fieldName] = try FieldValue(fieldData, fieldName: fieldName) } } diff --git a/Sources/MistKit/Models/RecordOperationFailure.swift b/Sources/MistKit/Models/RecordOperationFailure.swift new file mode 100644 index 00000000..43895ed7 --- /dev/null +++ b/Sources/MistKit/Models/RecordOperationFailure.swift @@ -0,0 +1,168 @@ +// +// RecordOperationFailure.swift +// MistKit +// +// Created by Leo Dion. +// Copyright © 2026 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +internal import MistKitOpenAPI + +/// A per-record failure returned inline in a CloudKit `modifyRecords` or +/// `lookupRecords` response. +/// +/// CloudKit reports per-operation failures as error entries within the +/// otherwise-successful (HTTP 200) `records` array, carrying the failed +/// record's name, a server error code, and optional retry/redirect hints. +/// +/// This is a data payload describing a failure, **not** a Swift `Error` type; +/// it is surfaced via ``RecordResult/failure(_:)`` from `modifyRecords` / +/// `lookupRecords`, and wrapped in ``CloudKitError/recordOperationFailed(_:)`` +/// (which *is* an `Error`) when a single-record convenience +/// (`createRecord`/`updateRecord`/`deleteRecord`) hits one. +/// +/// `RecordOperationFailure` is a MistKit-owned value, so callers can inspect a +/// failure with only `import MistKit` — no need to import the generated +/// `MistKitOpenAPI` module. +public struct RecordOperationFailure: Codable, Hashable, Sendable { + /// The CloudKit server error code for a per-record failure. + /// + /// Mirrors CloudKit's documented `serverErrorCode` values; an + /// ``unknown(_:)`` case carries any code not yet known to this version of + /// MistKit so forward-compatibility never drops information. + public enum ServerErrorCode: Codable, Hashable, Sendable { + case accessDenied + case atomicError + case authenticationFailed + case authenticationRequired + case badRequest + case conflict + case exists + case internalError + case notFound + case quotaExceeded + case throttled + case tryAgainLater + case validatingReferenceError + case zoneNotFound + /// A server error code not recognized by this version of MistKit. + case unknown(String) + + /// The known (case, raw CloudKit string) pairs — the single source of truth + /// for converting in both directions. + private static let knownPairs: [(code: ServerErrorCode, raw: String)] = [ + (.accessDenied, "ACCESS_DENIED"), + (.atomicError, "ATOMIC_ERROR"), + (.authenticationFailed, "AUTHENTICATION_FAILED"), + (.authenticationRequired, "AUTHENTICATION_REQUIRED"), + (.badRequest, "BAD_REQUEST"), + (.conflict, "CONFLICT"), + (.exists, "EXISTS"), + (.internalError, "INTERNAL_ERROR"), + (.notFound, "NOT_FOUND"), + (.quotaExceeded, "QUOTA_EXCEEDED"), + (.throttled, "THROTTLED"), + (.tryAgainLater, "TRY_AGAIN_LATER"), + (.validatingReferenceError, "VALIDATING_REFERENCE_ERROR"), + (.zoneNotFound, "ZONE_NOT_FOUND"), + ] + + /// The raw CloudKit string for this code (e.g. `"NOT_FOUND"`). + public var rawValue: String { + if case .unknown(let raw) = self { + return raw + } + return Self.knownPairs.first { $0.code == self }?.raw ?? "" + } + + /// Maps a raw CloudKit string to a known case, or ``unknown(_:)``. + public init(rawValue: String) { + self = Self.knownPairs.first { $0.raw == rawValue }?.code ?? .unknown(rawValue) + } + + /// Decodes the code from its raw CloudKit string value. + public init(from decoder: any Decoder) throws { + let container = try decoder.singleValueContainer() + self.init(rawValue: try container.decode(String.self)) + } + + /// Encodes the code as its raw CloudKit string value. + public func encode(to encoder: any Encoder) throws { + var container = encoder.singleValueContainer() + try container.encode(rawValue) + } + } + + /// The name of the record the operation failed on. + public let recordName: String + /// The CloudKit server error code for the failure. + public let serverErrorCode: ServerErrorCode + /// A human-readable reason for the failure, if provided. + public let reason: String? + /// Suggested seconds to wait before retrying. Absent if not retryable. + public let retryAfter: Int? + /// A unique identifier for this error. + public let uuid: String? + /// Redirect URL for sign-in; present when `serverErrorCode` is + /// ``ServerErrorCode/authenticationRequired``. + public let redirectURL: String? + + /// Creates a per-record failure value. + public init( + recordName: String, + serverErrorCode: ServerErrorCode, + reason: String? = nil, + retryAfter: Int? = nil, + uuid: String? = nil, + redirectURL: String? = nil + ) { + self.recordName = recordName + self.serverErrorCode = serverErrorCode + self.reason = reason + self.retryAfter = retryAfter + self.uuid = uuid + self.redirectURL = redirectURL + } + + internal init(from schema: Components.Schemas.RecordOperationFailure) { + let serverErrorCode = ServerErrorCode(rawValue: schema.serverErrorCode.rawValue) + // The generated `serverErrorCodePayload` is a closed enum mirroring the + // schema, so a `.unknown` here means our `knownPairs` table drifted from + // the regenerated schema — assert loudly (test-overridable) while still + // preserving the raw code for forward-compatibility in release. + if case .unknown(let raw) = serverErrorCode { + ConversionFailureReporter.assertionHandler( + "Unmapped CloudKit serverErrorCode \"\(raw)\" — update ServerErrorCode.knownPairs", + #fileID, + #line + ) + } + self.recordName = schema.recordName + self.serverErrorCode = serverErrorCode + self.reason = schema.reason + self.retryAfter = schema.retryAfter + self.uuid = schema.uuid + self.redirectURL = schema.redirectURL + } +} diff --git a/Sources/MistKit/Models/RecordResult.swift b/Sources/MistKit/Models/RecordResult.swift new file mode 100644 index 00000000..7bcec112 --- /dev/null +++ b/Sources/MistKit/Models/RecordResult.swift @@ -0,0 +1,88 @@ +// +// RecordResult.swift +// MistKit +// +// Created by Leo Dion. +// Copyright © 2026 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +internal import MistKitOpenAPI + +/// The outcome of a single operation in a `modifyRecords` or `lookupRecords` +/// batch. +/// +/// CloudKit returns per-operation results inline in the response `records` +/// array: a successful operation yields a record, while a failed one yields an +/// error describing what went wrong. `RecordResult` models that union so no +/// per-record failure is silently dropped. +/// +/// ```swift +/// let results = try await service.modifyRecords(operations, database: .private) +/// for result in results { +/// switch result { +/// case .success(let record): print("saved \(record.recordName)") +/// case .failure(let error): print("failed \(error.recordName): \(error.serverErrorCode.rawValue)") +/// } +/// } +/// ``` +public enum RecordResult: Sendable { + /// The operation succeeded and CloudKit returned the resulting record. + case success(RecordInfo) + /// The operation failed; the associated ``RecordOperationFailure`` describes + /// the failure. + case failure(RecordOperationFailure) + + internal init( + from item: Components.Schemas.ModifyResponse.recordsPayloadPayload + ) throws(ConversionError) { + switch item { + case .RecordOperationFailure(let error): + self = .failure(RecordOperationFailure(from: error)) + case .RecordResponse(let record): + self = .success(try RecordInfo(from: record)) + } + } + + internal init( + from item: Components.Schemas.LookupResponse.recordsPayloadPayload + ) throws(ConversionError) { + switch item { + case .RecordOperationFailure(let error): + self = .failure(RecordOperationFailure(from: error)) + case .RecordResponse(let record): + self = .success(try RecordInfo(from: record)) + } + } + + /// Returns the record for a successful result, or throws + /// ``CloudKitError/recordOperationFailed(_:)`` for a failure. + public func get() throws(CloudKitError) -> RecordInfo { + switch self { + case .success(let record): + return record + case .failure(let error): + throw CloudKitError.recordOperationFailed(error) + } + } +} diff --git a/Sources/MistKit/Models/RecordTimestamp.swift b/Sources/MistKit/Models/RecordTimestamp.swift index 085d3d88..90098108 100644 --- a/Sources/MistKit/Models/RecordTimestamp.swift +++ b/Sources/MistKit/Models/RecordTimestamp.swift @@ -28,7 +28,6 @@ // public import Foundation -internal import Logging internal import MistKitOpenAPI /// Timestamp information for record creation or modification @@ -38,15 +37,14 @@ public struct RecordTimestamp: Codable, Sendable { /// The record name of the user who performed the action public let userRecordName: String? - internal init(from schema: Components.Schemas.RecordTimestamp) { - self.timestamp = schema.timestamp.flatMap { millis in + internal init(from schema: Components.Schemas.RecordTimestamp) throws(ConversionError) { + if let millis = schema.timestamp { guard millis >= 0 else { - Logger(subsystem: .api).warning( - "Invalid negative timestamp (\(millis) ms) — returning nil" - ) - return nil + try ConversionError.negativeTimestamp(milliseconds: millis).reportAndThrow() } - return Date(timeIntervalSince1970: millis / 1_000.0) + self.timestamp = Date(timeIntervalSince1970: millis / 1_000.0) + } else { + self.timestamp = nil } self.userRecordName = schema.userRecordName } diff --git a/Sources/MistKit/Models/Users/UserIdentity.swift b/Sources/MistKit/Models/Users/UserIdentity.swift index ba5c2713..fc7c7df8 100644 --- a/Sources/MistKit/Models/Users/UserIdentity.swift +++ b/Sources/MistKit/Models/Users/UserIdentity.swift @@ -31,26 +31,30 @@ internal import MistKitOpenAPI /// A user identity returned by CloudKit discover endpoints (`users/discover`, `users/caller`). public struct UserIdentity: Codable, Sendable { - /// The record name of the user in the Users zone - public let userRecordName: String? + /// Whether the user is discoverable and, if so, their record name in the + /// Users zone. A non-discoverable user reports ``UserRecordName/nonDiscoverable``. + public let userRecordName: UserRecordName /// The user's name components (given name, family name, etc.) public let nameComponents: NameComponents? /// Lookup information used to discover this identity public let lookupInfo: UserIdentityLookupInfo? internal init(from schema: Components.Schemas.UserIdentity) { - self.userRecordName = schema.userRecordName + // CloudKit returns an identity with only `lookupInfo` for a + // non-discoverable user, so a missing record name is a valid response — + // not a conversion failure. + self.userRecordName = UserRecordName(schema.userRecordName) self.nameComponents = schema.nameComponents.map(NameComponents.init(from:)) self.lookupInfo = schema.lookupInfo.map(UserIdentityLookupInfo.init(from:)) } /// Initialize a user identity /// - Parameters: - /// - userRecordName: The record name of the user + /// - userRecordName: Whether the user is discoverable and their record name /// - nameComponents: The user's name components /// - lookupInfo: Lookup information for this identity public init( - userRecordName: String? = nil, + userRecordName: UserRecordName = .nonDiscoverable, nameComponents: NameComponents? = nil, lookupInfo: UserIdentityLookupInfo? = nil ) { diff --git a/Sources/MistKit/Models/Users/UserInfo.swift b/Sources/MistKit/Models/Users/UserInfo.swift index 2899f2c3..47d0872b 100644 --- a/Sources/MistKit/Models/Users/UserInfo.swift +++ b/Sources/MistKit/Models/Users/UserInfo.swift @@ -40,8 +40,33 @@ public struct UserInfo: Encodable, Sendable { /// The user's email address public let emailAddress: String? - internal init(from cloudKitUser: Components.Schemas.UserResponse) { - self.userRecordName = cloudKitUser.userRecordName ?? "Unknown" + /// Create a `UserInfo` directly. + /// + /// Primarily for testing and manual construction; instances returned by + /// CloudKit operations are built from the response internally. + /// + /// - Parameters: + /// - userRecordName: The user's record name. + /// - firstName: The user's first name, if known. + /// - lastName: The user's last name, if known. + /// - emailAddress: The user's email address, if known. + public init( + userRecordName: String, + firstName: String? = nil, + lastName: String? = nil, + emailAddress: String? = nil + ) { + self.userRecordName = userRecordName + self.firstName = firstName + self.lastName = lastName + self.emailAddress = emailAddress + } + + internal init(from cloudKitUser: Components.Schemas.UserResponse) throws(ConversionError) { + guard let userRecordName = cloudKitUser.userRecordName else { + try ConversionError.userMissingRecordName.reportAndThrow() + } + self.userRecordName = userRecordName self.firstName = cloudKitUser.firstName self.lastName = cloudKitUser.lastName self.emailAddress = cloudKitUser.emailAddress diff --git a/Sources/MistKit/Models/Users/UserRecordName.swift b/Sources/MistKit/Models/Users/UserRecordName.swift new file mode 100644 index 00000000..665fb047 --- /dev/null +++ b/Sources/MistKit/Models/Users/UserRecordName.swift @@ -0,0 +1,68 @@ +// +// UserRecordName.swift +// MistKit +// +// Created by Leo Dion. +// Copyright © 2026 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +/// The record-name slot of a ``UserIdentity``. +/// +/// CloudKit's discover endpoints return an identity's record name only when the +/// user is discoverable; a non-discoverable user comes back with `lookupInfo` +/// alone and no record name. Modeling that as an enum (rather than an optional +/// `String`) makes the two states explicit instead of overloading `nil`. +public enum UserRecordName: Codable, Sendable, Hashable { + /// The user is discoverable; the associated value is their record name in the + /// Users zone. + case recordName(String) + /// The user is not discoverable, so CloudKit returned no record name. + case nonDiscoverable + + /// Maps CloudKit's optional record-name string onto the two-state enum: a + /// present value is ``recordName(_:)``, a missing one is ``nonDiscoverable``. + internal init(_ recordName: String?) { + self = recordName.map(Self.recordName) ?? .nonDiscoverable + } + + /// Decodes a record name from a single string value, treating `null` as + /// ``nonDiscoverable`` so the JSON shape stays a plain optional string. + public init(from decoder: any Decoder) throws { + let container = try decoder.singleValueContainer() + if container.decodeNil() { + self = .nonDiscoverable + } else { + self = .recordName(try container.decode(String.self)) + } + } + + /// Encodes a record name as a string, or `null` when non-discoverable. + public func encode(to encoder: any Encoder) throws { + var container = encoder.singleValueContainer() + switch self { + case .recordName(let name): try container.encode(name) + case .nonDiscoverable: try container.encodeNil() + } + } +} diff --git a/Sources/MistKit/Models/Zones/ZoneChangesResult.swift b/Sources/MistKit/Models/Zones/ZoneChangesResult.swift index cbfd7168..a4cc6a06 100644 --- a/Sources/MistKit/Models/Zones/ZoneChangesResult.swift +++ b/Sources/MistKit/Models/Zones/ZoneChangesResult.swift @@ -52,18 +52,12 @@ public struct ZoneChangesResult: Codable, Sendable { self.moreComing = moreComing } - internal init(from response: Components.Schemas.ZoneChangesResponse) { - self.zones = - response.zones?.compactMap { zonePayload in - guard let zoneID = zonePayload.zoneID else { - return nil - } - return ZoneInfo( - zoneName: zoneID.zoneName ?? "Unknown", - ownerRecordName: zoneID.ownerName, - capabilities: [] // CloudKit Web Services zone-changes responses omit capabilities - ) - } ?? [] + internal init(from response: Components.Schemas.ZoneChangesResponse) throws(ConversionError) { + var zones: [ZoneInfo] = [] + for zone in response.zones ?? [] { + zones.append(try ZoneInfo(fromZoneID: zone.zoneID)) + } + self.zones = zones self.syncToken = response.syncToken self.moreComing = response.moreComing ?? false } diff --git a/Sources/MistKit/Models/Zones/ZoneInfo.swift b/Sources/MistKit/Models/Zones/ZoneInfo.swift index 6e6c6e88..fe0b2c07 100644 --- a/Sources/MistKit/Models/Zones/ZoneInfo.swift +++ b/Sources/MistKit/Models/Zones/ZoneInfo.swift @@ -27,6 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // +internal import MistKitOpenAPI + /// Zone information from CloudKit public struct ZoneInfo: Codable, Sendable { /// The zone name @@ -44,4 +46,31 @@ public struct ZoneInfo: Codable, Sendable { self.ownerRecordName = ownerRecordName self.capabilities = capabilities } + + /// Convert a CloudKit zone payload's `zoneID` into a `ZoneInfo`. + /// + /// All zone responses (`list`/`lookup`/`modify`/`changes`) expose an optional + /// `zoneID`; a missing `zoneID` or `zoneName` is a conversion failure (logged, + /// asserted in DEBUG, and thrown) rather than a silently-dropped zone. + /// + /// The optionality is *not* tightened to `required` in `openapi.yaml` on + /// purpose: `ZoneID` is one shared schema reused across request bodies (where + /// callers legitimately send a bare `zoneName` with no resolved `zoneID`) and + /// response bodies. Making the fields required would break request encoding + /// and make the generated decoder reject otherwise-valid payloads, so the + /// response-side "must be present" rule is enforced here at the domain + /// boundary instead. + internal init(fromZoneID zoneID: Components.Schemas.ZoneID?) throws(ConversionError) { + guard let zoneID else { + try ConversionError.zoneMissingID.reportAndThrow() + } + guard let zoneName = zoneID.zoneName else { + try ConversionError.zoneMissingName.reportAndThrow() + } + self.init( + zoneName: zoneName, + ownerRecordName: zoneID.ownerName, + capabilities: [] + ) + } } diff --git a/Sources/MistKitOpenAPI/Types.swift b/Sources/MistKitOpenAPI/Types.swift index 1235c05c..b55fe95c 100644 --- a/Sources/MistKitOpenAPI/Types.swift +++ b/Sources/MistKitOpenAPI/Types.swift @@ -1584,13 +1584,50 @@ public enum Components { } /// - Remark: Generated from `#/components/schemas/ModifyResponse`. public struct ModifyResponse: Codable, Hashable, Sendable { + /// - Remark: Generated from `#/components/schemas/ModifyResponse/recordsPayload`. + @frozen public enum recordsPayloadPayload: Codable, Hashable, Sendable { + /// - Remark: Generated from `#/components/schemas/ModifyResponse/recordsPayload/case1`. + case RecordOperationFailure(Components.Schemas.RecordOperationFailure) + /// - Remark: Generated from `#/components/schemas/ModifyResponse/recordsPayload/case2`. + case RecordResponse(Components.Schemas.RecordResponse) + public init(from decoder: any Decoder) throws { + var errors: [any Error] = [] + do { + self = .RecordOperationFailure(try .init(from: decoder)) + return + } catch { + errors.append(error) + } + do { + self = .RecordResponse(try .init(from: decoder)) + return + } catch { + errors.append(error) + } + throw Swift.DecodingError.failedToDecodeOneOfSchema( + type: Self.self, + codingPath: decoder.codingPath, + errors: errors + ) + } + public func encode(to encoder: any Encoder) throws { + switch self { + case let .RecordOperationFailure(value): + try value.encode(to: encoder) + case let .RecordResponse(value): + try value.encode(to: encoder) + } + } + } /// - Remark: Generated from `#/components/schemas/ModifyResponse/records`. - public var records: [Components.Schemas.RecordResponse]? + public typealias recordsPayload = [Components.Schemas.ModifyResponse.recordsPayloadPayload] + /// - Remark: Generated from `#/components/schemas/ModifyResponse/records`. + public var records: Components.Schemas.ModifyResponse.recordsPayload? /// Creates a new `ModifyResponse`. /// /// - Parameters: /// - records: - public init(records: [Components.Schemas.RecordResponse]? = nil) { + public init(records: Components.Schemas.ModifyResponse.recordsPayload? = nil) { self.records = records } public enum CodingKeys: String, CodingKey { @@ -1599,13 +1636,50 @@ public enum Components { } /// - Remark: Generated from `#/components/schemas/LookupResponse`. public struct LookupResponse: Codable, Hashable, Sendable { + /// - Remark: Generated from `#/components/schemas/LookupResponse/recordsPayload`. + @frozen public enum recordsPayloadPayload: Codable, Hashable, Sendable { + /// - Remark: Generated from `#/components/schemas/LookupResponse/recordsPayload/case1`. + case RecordOperationFailure(Components.Schemas.RecordOperationFailure) + /// - Remark: Generated from `#/components/schemas/LookupResponse/recordsPayload/case2`. + case RecordResponse(Components.Schemas.RecordResponse) + public init(from decoder: any Decoder) throws { + var errors: [any Error] = [] + do { + self = .RecordOperationFailure(try .init(from: decoder)) + return + } catch { + errors.append(error) + } + do { + self = .RecordResponse(try .init(from: decoder)) + return + } catch { + errors.append(error) + } + throw Swift.DecodingError.failedToDecodeOneOfSchema( + type: Self.self, + codingPath: decoder.codingPath, + errors: errors + ) + } + public func encode(to encoder: any Encoder) throws { + switch self { + case let .RecordOperationFailure(value): + try value.encode(to: encoder) + case let .RecordResponse(value): + try value.encode(to: encoder) + } + } + } /// - Remark: Generated from `#/components/schemas/LookupResponse/records`. - public var records: [Components.Schemas.RecordResponse]? + public typealias recordsPayload = [Components.Schemas.LookupResponse.recordsPayloadPayload] + /// - Remark: Generated from `#/components/schemas/LookupResponse/records`. + public var records: Components.Schemas.LookupResponse.recordsPayload? /// Creates a new `LookupResponse`. /// /// - Parameters: /// - records: - public init(records: [Components.Schemas.RecordResponse]? = nil) { + public init(records: Components.Schemas.LookupResponse.recordsPayload? = nil) { self.records = records } public enum CodingKeys: String, CodingKey { @@ -2132,6 +2206,90 @@ public enum Components { case webcAuthToken } } + /// Per-record error returned inline in the `records` array of a 200 + /// modify/lookup response. Identifies the record that failed and why. + /// Distinct from `ErrorResponse`, which is the body of a top-level 4xx/5xx + /// HTTP failure. Note CloudKit does not echo `recordType` on a record error. + /// + /// + /// - Remark: Generated from `#/components/schemas/RecordOperationFailure`. + public struct RecordOperationFailure: Codable, Hashable, Sendable { + /// The name of the record that the operation failed on. + /// + /// - Remark: Generated from `#/components/schemas/RecordOperationFailure/recordName`. + public var recordName: Swift.String + /// The code for the error that occurred. + /// + /// - Remark: Generated from `#/components/schemas/RecordOperationFailure/serverErrorCode`. + @frozen public enum serverErrorCodePayload: String, Codable, Hashable, Sendable, CaseIterable { + case ACCESS_DENIED = "ACCESS_DENIED" + case ATOMIC_ERROR = "ATOMIC_ERROR" + case AUTHENTICATION_FAILED = "AUTHENTICATION_FAILED" + case AUTHENTICATION_REQUIRED = "AUTHENTICATION_REQUIRED" + case BAD_REQUEST = "BAD_REQUEST" + case CONFLICT = "CONFLICT" + case EXISTS = "EXISTS" + case INTERNAL_ERROR = "INTERNAL_ERROR" + case NOT_FOUND = "NOT_FOUND" + case QUOTA_EXCEEDED = "QUOTA_EXCEEDED" + case THROTTLED = "THROTTLED" + case TRY_AGAIN_LATER = "TRY_AGAIN_LATER" + case VALIDATING_REFERENCE_ERROR = "VALIDATING_REFERENCE_ERROR" + case ZONE_NOT_FOUND = "ZONE_NOT_FOUND" + } + /// The code for the error that occurred. + /// + /// - Remark: Generated from `#/components/schemas/RecordOperationFailure/serverErrorCode`. + public var serverErrorCode: Components.Schemas.RecordOperationFailure.serverErrorCodePayload + /// A string indicating the reason for the error. + /// + /// - Remark: Generated from `#/components/schemas/RecordOperationFailure/reason`. + public var reason: Swift.String? + /// Suggested seconds to wait before retrying. Absent if not retryable. + /// + /// - Remark: Generated from `#/components/schemas/RecordOperationFailure/retryAfter`. + public var retryAfter: Swift.Int? + /// A unique identifier for this error. + /// + /// - Remark: Generated from `#/components/schemas/RecordOperationFailure/uuid`. + public var uuid: Swift.String? + /// Redirect URL for sign-in; present when serverErrorCode is AUTHENTICATION_REQUIRED. + /// + /// - Remark: Generated from `#/components/schemas/RecordOperationFailure/redirectURL`. + public var redirectURL: Swift.String? + /// Creates a new `RecordOperationFailure`. + /// + /// - Parameters: + /// - recordName: The name of the record that the operation failed on. + /// - serverErrorCode: The code for the error that occurred. + /// - reason: A string indicating the reason for the error. + /// - retryAfter: Suggested seconds to wait before retrying. Absent if not retryable. + /// - uuid: A unique identifier for this error. + /// - redirectURL: Redirect URL for sign-in; present when serverErrorCode is AUTHENTICATION_REQUIRED. + public init( + recordName: Swift.String, + serverErrorCode: Components.Schemas.RecordOperationFailure.serverErrorCodePayload, + reason: Swift.String? = nil, + retryAfter: Swift.Int? = nil, + uuid: Swift.String? = nil, + redirectURL: Swift.String? = nil + ) { + self.recordName = recordName + self.serverErrorCode = serverErrorCode + self.reason = reason + self.retryAfter = retryAfter + self.uuid = uuid + self.redirectURL = redirectURL + } + public enum CodingKeys: String, CodingKey { + case recordName + case serverErrorCode + case reason + case retryAfter + case uuid + case redirectURL + } + } /// Error response object. For a full list of error codes and meanings, see: /// https://developer.apple.com/library/archive/documentation/DataManagement/Conceptual/CloudKitWebServicesReference/ErrorCodes.html#//apple_ref/doc/uid/TP40015240-CH4-SW1 /// diff --git a/Tests/MistKitTests/CloudKitService/DiscoverUserIdentities/CloudKitServiceTests.DiscoverUserIdentities+SuccessCases.swift b/Tests/MistKitTests/CloudKitService/DiscoverUserIdentities/CloudKitServiceTests.DiscoverUserIdentities+SuccessCases.swift index 6aa3dc1f..7a2e7f14 100644 --- a/Tests/MistKitTests/CloudKitService/DiscoverUserIdentities/CloudKitServiceTests.DiscoverUserIdentities+SuccessCases.swift +++ b/Tests/MistKitTests/CloudKitService/DiscoverUserIdentities/CloudKitServiceTests.DiscoverUserIdentities+SuccessCases.swift @@ -50,7 +50,7 @@ extension CloudKitServiceTests.DiscoverUserIdentities { ) #expect(identities.count == 1) - #expect(identities.first?.userRecordName == "_user-0") + #expect(identities.first?.userRecordName == .recordName("_user-0")) } @Test("discoverUserIdentities() returns multiple identities") @@ -72,9 +72,9 @@ extension CloudKitServiceTests.DiscoverUserIdentities { ) #expect(identities.count == 3) - #expect(identities[0].userRecordName == "_user-0") - #expect(identities[1].userRecordName == "_user-1") - #expect(identities[2].userRecordName == "_user-2") + #expect(identities[0].userRecordName == .recordName("_user-0")) + #expect(identities[1].userRecordName == .recordName("_user-1")) + #expect(identities[2].userRecordName == .recordName("_user-2")) } @Test("discoverUserIdentities() returns empty array when no matches") diff --git a/Tests/MistKitTests/CloudKitService/FetchZoneChanges/CloudKitServiceTests.FetchZoneChanges+SuccessCases.swift b/Tests/MistKitTests/CloudKitService/FetchZoneChanges/CloudKitServiceTests.FetchZoneChanges+SuccessCases.swift index ce40da10..c76b52d3 100644 --- a/Tests/MistKitTests/CloudKitService/FetchZoneChanges/CloudKitServiceTests.FetchZoneChanges+SuccessCases.swift +++ b/Tests/MistKitTests/CloudKitService/FetchZoneChanges/CloudKitServiceTests.FetchZoneChanges+SuccessCases.swift @@ -103,8 +103,8 @@ extension CloudKitServiceTests.FetchZoneChanges { #expect(result.syncToken == "new-token") } - @Test("fetchZoneChanges() filters out zones with nil zoneID from server response") - internal func fetchZoneChangesFiltersNilZoneID() async throws { + @Test("fetchZoneChanges() throws when the server returns a zone with nil zoneID") + internal func fetchZoneChangesThrowsOnNilZoneID() async throws { guard #available(macOS 11.0, iOS 14.0, tvOS 14.0, watchOS 7.0, *) else { Issue.record("CloudKitService is not available on this operating system.") return @@ -119,10 +119,15 @@ extension CloudKitServiceTests.FetchZoneChanges { transport: transport ) - let result = try await service.fetchZoneChanges(database: .public(.prefers(.serverToServer))) - - #expect(result.zones.count == 1, "Zone with nil zoneID should be filtered out") - #expect(result.zones.first?.zoneName == "valid-zone") + // Suppress the DEBUG assertion trap so the thrown error is observable. + await ConversionFailureReporter.$assertionHandler.withValue( + { _, _, _ in }, + operation: { + await #expect(throws: CloudKitError.self) { + _ = try await service.fetchZoneChanges(database: .public(.prefers(.serverToServer))) + } + } + ) } } } diff --git a/Tests/MistKitTests/CloudKitService/LookupUsersByEmail/CloudKitServiceTests.LookupUsersByEmail+SuccessCases.swift b/Tests/MistKitTests/CloudKitService/LookupUsersByEmail/CloudKitServiceTests.LookupUsersByEmail+SuccessCases.swift index 3796a072..bad1969d 100644 --- a/Tests/MistKitTests/CloudKitService/LookupUsersByEmail/CloudKitServiceTests.LookupUsersByEmail+SuccessCases.swift +++ b/Tests/MistKitTests/CloudKitService/LookupUsersByEmail/CloudKitServiceTests.LookupUsersByEmail+SuccessCases.swift @@ -47,7 +47,7 @@ extension CloudKitServiceTests.LookupUsersByEmail { let identities = try await service.lookupUsersByEmail(["user@example.com"]) #expect(identities.count == 1) - #expect(identities.first?.userRecordName == "_user-0") + #expect(identities.first?.userRecordName == .recordName("_user-0")) } @Test("lookupUsersByEmail() returns multiple identities") diff --git a/Tests/MistKitTests/CloudKitService/LookupUsersByRecordName/CloudKitServiceTests.LookupUsersByRecordName+SuccessCases.swift b/Tests/MistKitTests/CloudKitService/LookupUsersByRecordName/CloudKitServiceTests.LookupUsersByRecordName+SuccessCases.swift index 063dbe0b..11ef8fc0 100644 --- a/Tests/MistKitTests/CloudKitService/LookupUsersByRecordName/CloudKitServiceTests.LookupUsersByRecordName+SuccessCases.swift +++ b/Tests/MistKitTests/CloudKitService/LookupUsersByRecordName/CloudKitServiceTests.LookupUsersByRecordName+SuccessCases.swift @@ -47,7 +47,7 @@ extension CloudKitServiceTests.LookupUsersByRecordName { let identities = try await service.lookupUsersByRecordName(["_user-0"]) #expect(identities.count == 1) - #expect(identities.first?.userRecordName == "_user-0") + #expect(identities.first?.userRecordName == .recordName("_user-0")) } @Test("lookupUsersByRecordName() returns multiple identities") diff --git a/Tests/MistKitTests/CloudKitService/LookupZones/CloudKitServiceTests.LookupZones+SuccessCases.swift b/Tests/MistKitTests/CloudKitService/LookupZones/CloudKitServiceTests.LookupZones+SuccessCases.swift index 8ec552e5..d00b7f79 100644 --- a/Tests/MistKitTests/CloudKitService/LookupZones/CloudKitServiceTests.LookupZones+SuccessCases.swift +++ b/Tests/MistKitTests/CloudKitService/LookupZones/CloudKitServiceTests.LookupZones+SuccessCases.swift @@ -91,8 +91,8 @@ extension CloudKitServiceTests.LookupZones { #expect(zones.isEmpty) } - @Test("lookupZones() drops zones with missing zoneName instead of substituting placeholder") - internal func lookupZonesDropsZonesWithoutName() async throws { + @Test("lookupZones() throws when a returned zone is missing its zoneName") + internal func lookupZonesThrowsOnZoneWithoutName() async throws { guard #available(macOS 11.0, iOS 14.0, tvOS 14.0, watchOS 7.0, *) else { Issue.record("CloudKitService is not available on this operating system.") return @@ -100,14 +100,18 @@ extension CloudKitServiceTests.LookupZones { let service = try await CloudKitServiceTests.LookupZones.makeServiceReturningZoneWithoutName() - let zones = try await service.lookupZones( - zoneIDs: [ZoneID(zoneName: "valid-zone", ownerName: nil)], - database: .public(.prefers(.serverToServer)) + // Suppress the DEBUG assertion trap so the thrown error is observable. + await ConversionFailureReporter.$assertionHandler.withValue( + { _, _, _ in }, + operation: { + await #expect(throws: CloudKitError.self) { + _ = try await service.lookupZones( + zoneIDs: [ZoneID(zoneName: "valid-zone", ownerName: nil)], + database: .public(.prefers(.serverToServer)) + ) + } + } ) - - #expect(zones.count == 1) - #expect(zones.first?.zoneName == "valid-zone") - #expect(!zones.contains { $0.zoneName == "Unknown" }) } } } diff --git a/Tests/MistKitTests/Models/BatchSyncResultTests.swift b/Tests/MistKitTests/Models/BatchSyncResultTests.swift index b6bc241d..840c44f9 100644 --- a/Tests/MistKitTests/Models/BatchSyncResultTests.swift +++ b/Tests/MistKitTests/Models/BatchSyncResultTests.swift @@ -28,7 +28,6 @@ // internal import Foundation -internal import MistKitOpenAPI internal import Testing @testable import MistKit @@ -49,12 +48,22 @@ internal struct BatchSyncResultTests { ) } - /// Builds an error `RecordInfo` via the same response-decoding path used in - /// production (`RecordInfo.init(from:)` with an empty `RecordResponse`), - /// rather than hardcoding the "Unknown" sentinel string in the test. - /// This matches the pattern in `RecordInfoTests.recordInfoWithUnknownRecord`. - internal static func makeErrorRecord() -> RecordInfo { - RecordInfo(from: Components.Schemas.RecordResponse()) + /// A successful per-record result wrapping a plain record. + internal static func makeSuccess(name: String) -> RecordResult { + .success(makeRecord(name: name)) + } + + /// A per-record error payload, as CloudKit returns inline in a batch. + internal static func makeError( + name: String, + code: RecordOperationFailure.ServerErrorCode = .badRequest + ) -> RecordOperationFailure { + RecordOperationFailure(recordName: name, serverErrorCode: code) + } + + /// A failed per-record result. + internal static func makeFailure(name: String) -> RecordResult { + .failure(makeError(name: name)) } // MARK: - Tests @@ -65,15 +74,15 @@ internal struct BatchSyncResultTests { creates: ["new-1", "new-2"], updates: ["existing-1"] ) - let records: [RecordInfo] = [ - Self.makeRecord(name: "new-1"), - Self.makeRecord(name: "existing-1"), - Self.makeRecord(name: "new-2"), - Self.makeRecord(name: "server-assigned-name"), - Self.makeErrorRecord(), + let results: [RecordResult] = [ + Self.makeSuccess(name: "new-1"), + Self.makeSuccess(name: "existing-1"), + Self.makeSuccess(name: "new-2"), + Self.makeSuccess(name: "server-assigned-name"), + Self.makeFailure(name: "err-1"), ] - let result = BatchSyncResult(records: records, classification: classification) + let result = BatchSyncResult(results: results, classification: classification) #expect(result.createdCount == 2) #expect(result.updatedCount == 1) @@ -90,14 +99,14 @@ internal struct BatchSyncResultTests { creates: ["a"], updates: ["b"] ) - let records: [RecordInfo] = [ - Self.makeRecord(name: "a"), - Self.makeRecord(name: "b"), - Self.makeRecord(name: "c"), - Self.makeErrorRecord(), + let results: [RecordResult] = [ + Self.makeSuccess(name: "a"), + Self.makeSuccess(name: "b"), + Self.makeSuccess(name: "c"), + Self.makeFailure(name: "err-1"), ] - let result = BatchSyncResult(records: records, classification: classification) + let result = BatchSyncResult(results: results, classification: classification) #expect(result.totalCount == 4) #expect(result.succeededCount == 3) @@ -106,18 +115,17 @@ internal struct BatchSyncResultTests { + result.failedCount + result.unclassifiedCount) } - @Test("treats error records as failures regardless of classification") + @Test("treats error results as failures regardless of classification") internal func treatsErrorRecordsAsFailures() { - // Build a classification that *would* claim the error record as a create - // by reading its actual recordName, then verify the failure check wins. - let errorRecord = Self.makeErrorRecord() + // A classification that *would* claim the failed record's name as a create + // must not pull a `.failure` out of the `failed` bucket. let classification = OperationClassification( - creates: [errorRecord.recordName], + creates: ["err-1"], updates: [] ) let result = BatchSyncResult( - records: [errorRecord], + results: [Self.makeFailure(name: "err-1")], classification: classification ) @@ -128,7 +136,7 @@ internal struct BatchSyncResultTests { @Test("returns empty buckets for empty inputs") internal func returnsEmptyBucketsForEmptyInputs() { let classification = OperationClassification(creates: [], updates: []) - let result = BatchSyncResult(records: [], classification: classification) + let result = BatchSyncResult(results: [], classification: classification) #expect(result.totalCount == 0) #expect(result.succeededCount == 0) @@ -143,7 +151,7 @@ internal struct BatchSyncResultTests { let result = BatchSyncResult( created: [Self.makeRecord(name: "a")], updated: [Self.makeRecord(name: "b"), Self.makeRecord(name: "c")], - failed: [Self.makeErrorRecord()] + failed: [Self.makeError(name: "err-1")] ) #expect(result.createdCount == 1) diff --git a/Tests/MistKitTests/Models/ConversionFailureTests.swift b/Tests/MistKitTests/Models/ConversionFailureTests.swift new file mode 100644 index 00000000..6794c63a --- /dev/null +++ b/Tests/MistKitTests/Models/ConversionFailureTests.swift @@ -0,0 +1,204 @@ +// +// ConversionFailureTests.swift +// MistKit +// +// Created by Leo Dion. +// Copyright © 2026 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +internal import Foundation +internal import MistKitOpenAPI +internal import Testing + +@testable import MistKit + +/// Verifies that response→domain conversions fail loudly (throwing +/// `CloudKitError`) instead of silently dropping or masking data. The DEBUG +/// assertion trap is suppressed so the thrown error is observable. +@Suite("Conversion Failures") +internal struct ConversionFailureTests { + /// Runs `body`, expecting it to throw a `ConversionError`, with the DEBUG + /// assertion handler suppressed so the throw is observed rather than trapped. + private func expectConversionThrow( + _ body: () throws -> Void + ) { + ConversionFailureReporter.$assertionHandler.withValue( + { _, _, _ in }, + operation: { + #expect(throws: ConversionError.self) { + try body() + } + } + ) + } + + @Test("FieldValue throws on a reference field missing recordName") + internal func referenceMissingRecordNameThrows() { + let response = Components.Schemas.FieldValueResponse( + value: .ReferenceValue(.init(recordName: nil)) + ) + expectConversionThrow { + _ = try FieldValue(response, fieldName: "owner") + } + } + + @Test("RecordInfo throws on a field that cannot be mapped") + internal func recordInfoThrowsOnReferenceWithoutRecordName() { + let record = Components.Schemas.RecordResponse( + recordName: "rec-1", + recordType: "Article", + fields: .init(additionalProperties: [ + "owner": .init(value: .ReferenceValue(.init(recordName: nil))) + ]) + ) + expectConversionThrow { + _ = try RecordInfo(from: record) + } + } + + @Test("UserIdentity faithfully converts a non-discoverable user (nil userRecordName)") + internal func userIdentityWithoutRecordNameIsFaithful() { + // A non-discoverable user comes back with only lookupInfo; userRecordName + // is legitimately nil and must not be treated as a conversion failure. + let schema = Components.Schemas.UserIdentity() + let identity = UserIdentity(from: schema) + + #expect(identity.userRecordName == .nonDiscoverable) + } + + @Test("UserInfo throws when userRecordName is missing") + internal func userInfoMissingRecordNameThrows() { + let schema = Components.Schemas.UserResponse() + expectConversionThrow { + _ = try UserInfo(from: schema) + } + } + + @Test("RecordTimestamp throws on a negative timestamp") + internal func recordTimestampNegativeThrows() { + let schema = Components.Schemas.RecordTimestamp(timestamp: -1) + expectConversionThrow { + _ = try RecordTimestamp(from: schema) + } + } + + @Test("RecordResult maps an error item to .failure with the server error code") + internal func recordResultMapsErrorItem() throws { + let item = Components.Schemas.ModifyResponse.recordsPayloadPayload.RecordOperationFailure( + .init(recordName: "rec-1", serverErrorCode: .NOT_FOUND) + ) + let result = try RecordResult(from: item) + + guard case .failure(let error) = result else { + Issue.record("Expected .failure, got \(result)") + return + } + #expect(error.recordName == "rec-1") + #expect(error.serverErrorCode == .notFound) + } + + @Test("RecordResult maps a record item to .success") + internal func recordResultMapsRecordItem() throws { + let item = Components.Schemas.ModifyResponse.recordsPayloadPayload.RecordResponse( + .init(recordName: "rec-1", recordType: "Article") + ) + let result = try RecordResult(from: item) + + guard case .success(let record) = result else { + Issue.record("Expected .success, got \(result)") + return + } + #expect(record.recordName == "rec-1") + } + + @Test("RecordResult.get() rethrows a failure as recordOperationFailed") + internal func recordResultGetThrowsOnFailure() { + let result = RecordResult.failure( + RecordOperationFailure(recordName: "rec-1", serverErrorCode: .badRequest) + ) + #expect(throws: CloudKitError.self) { + _ = try result.get() + } + } + + @Test("BatchSyncResult partitions a mixed success/failure batch by classification") + internal func batchSyncResultPartitionsMixedResults() throws { + func successResult(_ recordName: String) throws -> RecordResult { + try RecordResult( + from: Components.Schemas.ModifyResponse.recordsPayloadPayload.RecordResponse( + .init(recordName: recordName, recordType: "Article") + ) + ) + } + let createdRecord = try successResult("new-1") + let updatedRecord = try successResult("existing-1") + let anonymousRecord = try successResult("server-assigned") + let failure = RecordResult.failure( + RecordOperationFailure(recordName: "bad-1", serverErrorCode: .notFound) + ) + + let classification = OperationClassification( + creates: ["new-1"], + updates: ["existing-1"] + ) + let batch = BatchSyncResult( + results: [createdRecord, updatedRecord, anonymousRecord, failure], + classification: classification + ) + + #expect(batch.created.map(\.recordName) == ["new-1"]) + #expect(batch.updated.map(\.recordName) == ["existing-1"]) + #expect(batch.unclassified.map(\.recordName) == ["server-assigned"]) + #expect(batch.failed.map(\.recordName) == ["bad-1"]) + // Every input result lands in exactly one bucket. + #expect(batch.totalCount == 4) + #expect(batch.succeededCount == 3) + #expect(batch.failedCount == 1) + } + + @Test("mapToCloudKitError promotes a thrown ConversionError to .conversionFailed") + internal func mapToCloudKitErrorPromotesConversionError() throws { + let service = try CloudKitService( + containerIdentifier: TestConstants.serviceContainerIdentifier, + credentials: Credentials( + apiAuth: APICredentials( + apiToken: TestConstants.apiToken, + webAuthToken: TestConstants.webAuthToken + ) + ), + transport: MockTransport() + ) + + let mapped = service.mapToCloudKitError( + ConversionError.zoneMissingName, + context: "test" + ) + + guard case .conversionFailed(let conversionError) = mapped else { + Issue.record("Expected .conversionFailed, got \(mapped)") + return + } + #expect(conversionError == .zoneMissingName) + } +} diff --git a/Tests/MistKitTests/Models/RecordInfoTests.swift b/Tests/MistKitTests/Models/RecordInfoTests.swift index 26423fe3..4231616d 100644 --- a/Tests/MistKitTests/Models/RecordInfoTests.swift +++ b/Tests/MistKitTests/Models/RecordInfoTests.swift @@ -7,14 +7,33 @@ internal import Testing @Suite("Record Info") /// Tests for RecordInfo functionality internal struct RecordInfoTests { - /// Tests RecordInfo initialization with empty record data - @Test("RecordInfo initialization with empty record data") - internal func recordInfoWithUnknownRecord() { + /// A response with no identifiers is a conversion failure, not an "Unknown" + /// record. The assertion handler is suppressed so the throw is observable in + /// DEBUG instead of trapping the test process. + @Test("RecordInfo throws when the response is missing identifiers") + internal func recordInfoMissingIdentifiersThrows() { let mockRecord = Components.Schemas.RecordResponse() - let recordInfo = RecordInfo(from: mockRecord) + ConversionFailureReporter.$assertionHandler.withValue( + { _, _, _ in }, + operation: { + #expect(throws: ConversionError.self) { + _ = try RecordInfo(from: mockRecord) + } + } + ) + } + + /// A well-formed response converts to a RecordInfo with its identifiers. + @Test("RecordInfo converts a well-formed response") + internal func recordInfoFromValidRecord() throws { + let mockRecord = Components.Schemas.RecordResponse( + recordName: "rec-1", + recordType: "Article" + ) + let recordInfo = try RecordInfo(from: mockRecord) - #expect(recordInfo.recordName == "Unknown") - #expect(recordInfo.recordType == "Unknown") + #expect(recordInfo.recordName == "rec-1") + #expect(recordInfo.recordType == "Article") #expect(recordInfo.fields.isEmpty) } } diff --git a/Tests/MistKitTests/Models/ServerErrorCodeTests.swift b/Tests/MistKitTests/Models/ServerErrorCodeTests.swift new file mode 100644 index 00000000..82f2e584 --- /dev/null +++ b/Tests/MistKitTests/Models/ServerErrorCodeTests.swift @@ -0,0 +1,63 @@ +// +// ServerErrorCodeTests.swift +// MistKit +// +// Created by Leo Dion. +// Copyright © 2026 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +internal import Testing + +@testable import MistKit + +/// Verifies the `RecordOperationFailure.ServerErrorCode` ↔ raw CloudKit string +/// mapping is total in both directions and forward-compatible. +@Suite("ServerErrorCode") +internal struct ServerErrorCodeTests { + @Test( + "round-trips every known raw CloudKit string", + arguments: [ + "ACCESS_DENIED", "ATOMIC_ERROR", "AUTHENTICATION_FAILED", + "AUTHENTICATION_REQUIRED", "BAD_REQUEST", "CONFLICT", "EXISTS", + "INTERNAL_ERROR", "NOT_FOUND", "QUOTA_EXCEEDED", "THROTTLED", + "TRY_AGAIN_LATER", "VALIDATING_REFERENCE_ERROR", "ZONE_NOT_FOUND", + ] + ) + internal func roundTrips(rawValue: String) { + let code = RecordOperationFailure.ServerErrorCode(rawValue: rawValue) + // A known raw must map to a concrete case, not the forward-compat fallback… + if case .unknown = code { + Issue.record("\(rawValue) decoded as .unknown; missing from knownPairs") + } + // …and re-encode back to the identical raw string. + #expect(code.rawValue == rawValue) + } + + @Test("preserves an unrecognized code via .unknown") + internal func preservesUnknown() { + let code = RecordOperationFailure.ServerErrorCode(rawValue: "FUTURE_CODE") + #expect(code == .unknown("FUTURE_CODE")) + #expect(code.rawValue == "FUTURE_CODE") + } +} diff --git a/openapi.yaml b/openapi.yaml index b92a6d28..b6db4652 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -1220,7 +1220,9 @@ components: records: type: array items: - $ref: '#/components/schemas/RecordResponse' + oneOf: + - $ref: '#/components/schemas/RecordOperationFailure' + - $ref: '#/components/schemas/RecordResponse' LookupResponse: type: object @@ -1228,7 +1230,9 @@ components: records: type: array items: - $ref: '#/components/schemas/RecordResponse' + oneOf: + - $ref: '#/components/schemas/RecordOperationFailure' + - $ref: '#/components/schemas/RecordResponse' ChangesResponse: type: object @@ -1429,6 +1433,51 @@ components: webcAuthToken: type: string + RecordOperationFailure: + type: object + description: | + Per-record error returned inline in the `records` array of a 200 + modify/lookup response. Identifies the record that failed and why. + Distinct from `ErrorResponse`, which is the body of a top-level 4xx/5xx + HTTP failure. Note CloudKit does not echo `recordType` on a record error. + properties: + recordName: + type: string + description: The name of the record that the operation failed on. + serverErrorCode: + type: string + enum: + - ACCESS_DENIED + - ATOMIC_ERROR + - AUTHENTICATION_FAILED + - AUTHENTICATION_REQUIRED + - BAD_REQUEST + - CONFLICT + - EXISTS + - INTERNAL_ERROR + - NOT_FOUND + - QUOTA_EXCEEDED + - THROTTLED + - TRY_AGAIN_LATER + - VALIDATING_REFERENCE_ERROR + - ZONE_NOT_FOUND + description: The code for the error that occurred. + reason: + type: string + description: A string indicating the reason for the error. + retryAfter: + type: integer + description: Suggested seconds to wait before retrying. Absent if not retryable. + uuid: + type: string + description: A unique identifier for this error. + redirectURL: + type: string + description: Redirect URL for sign-in; present when serverErrorCode is AUTHENTICATION_REQUIRED. + required: + - recordName + - serverErrorCode + ErrorResponse: type: object description: | From 4d142afdd085fb80b665b2137a1ad33052942006 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 21 May 2026 19:32:04 +0000 Subject: [PATCH 12/35] Fix integration tests to use existing Note record type The live MistDemo integration tests targeted a CloudKit record type "MistKitIntegrationTest" that is not defined in the container schema, causing CI failures. The deployed schema only defines a Note record type with title/index/image fields, which the phases already populate. https://claude.ai/code/session_01EvLrWZwcSs1MjiCrUx8KjU --- .../Sources/MistDemoKit/Integration/IntegrationTestData.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Examples/MistDemo/Sources/MistDemoKit/Integration/IntegrationTestData.swift b/Examples/MistDemo/Sources/MistDemoKit/Integration/IntegrationTestData.swift index a4a86c16..0727df60 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Integration/IntegrationTestData.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Integration/IntegrationTestData.swift @@ -32,7 +32,7 @@ internal import Foundation /// Test data generation utilities for integration tests. internal enum IntegrationTestData { /// CloudKit record type for integration tests. - internal static let recordType = "MistKitIntegrationTest" + internal static let recordType = "Note" /// Generate minimal PNG-like binary data for upload testing. /// From 1376c8c735c7366ad68b13a060b1bc539f67eceb Mon Sep 17 00:00:00 2001 From: leogdion Date: Thu, 21 May 2026 16:23:30 -0400 Subject: [PATCH 13/35] Scaffold MistDemo (CLI + App + Web) for v1.0.0-beta.2 endpoints (#371) --- Examples/MistDemo/.swiftlint.yml | 4 + Examples/MistDemo/App/MistDemoApp.swift | 5 + Examples/MistDemo/MistDemoApp.entitlements | 4 + Examples/MistDemo/Package.swift | 2 + .../Models/RecordZoneChangesSnapshot.swift | 42 + .../MistDemoApp/Models/ResolveResult.swift | 47 + .../Services/CloudKitStore+Assets.swift | 90 ++ .../Services/CloudKitStore+PushTokens.swift | 94 ++ .../Services/CloudKitStore+Records.swift | 130 ++ .../CloudKitStore+Subscriptions.swift | 115 ++ .../Services/CloudKitStore+Users.swift | 123 ++ .../Services/CloudKitStore+Zones.swift | 99 ++ .../MistDemoApp/Services/CloudKitStore.swift | 13 + .../Services/PlatformAliases.swift | 84 ++ ...cationDelegate+NSApplicationDelegate.swift | 43 + ...licationDelegate+RemoteNotifications.swift | 59 + ...cationDelegate+UIApplicationDelegate.swift | 45 + ...cationDelegate+WKApplicationDelegate.swift | 54 + .../PlatformApplicationDelegate.swift | 48 + .../Services/PushNotificationDelegate.swift | 69 + .../Services/PushTokenReceiver.swift | 47 + .../RemoteNotificationRegistering.swift | 65 + .../MistDemoApp/Views/AssetsView.swift | 158 +++ .../Views/CompositionDisclosure.swift | 80 ++ .../MistDemoApp/Views/DetailColumnRoot.swift | 12 +- .../MistDemoApp/Views/PushTokensView.swift | 109 ++ .../MistDemoApp/Views/RecordsView.swift | 189 +++ .../MistDemoApp/Views/SidebarItem.swift | 15 + .../MistDemoApp/Views/SubscriptionsView.swift | 156 +++ .../Sources/MistDemoApp/Views/UsersView.swift | 172 +++ .../Views/View+DeleteSwipeAction.swift | 52 + .../MistDemoApp/Views/ZoneListView.swift | 77 +- .../Commands/CreateTokenCommand.swift | 69 + .../Commands/ListSubscriptionsCommand.swift | 68 + .../Commands/LookupSubscriptionCommand.swift | 68 + .../Commands/QueryCommand+FilterParsing.swift | 44 +- .../Commands/RegisterTokenCommand.swift | 70 + .../Commands/RereferenceAssetCommand.swift | 74 + .../MistDemoKit/Commands/ResolveCommand.swift | 74 + .../Configuration/CreateTokenConfig.swift | 91 ++ .../ListSubscriptionsConfig.swift | 78 ++ .../LookupSubscriptionConfig.swift | 93 ++ .../Configuration/RegisterTokenConfig.swift | 91 ++ .../RereferenceAssetConfig.swift | 101 ++ .../Configuration/ResolveConfig.swift | 94 ++ .../Integration/Phases/CreateTokenPhase.swift | 48 + .../Phases/ListSubscriptionsPhase.swift | 48 + .../Phases/LookupSubscriptionPhase.swift | 48 + .../Phases/RegisterTokenPhase.swift | 48 + .../Phases/RereferenceAssetPhase.swift | 48 + .../Phases/ResolveRecordsPhase.swift | 48 + .../Sources/MistDemoKit/MistDemoRunner.swift | 9 + .../Sources/MistDemoKit/PushTokenStatus.swift | 38 + .../Sources/MistDemoKit/Resources/index.html | 1235 ++++------------- .../Sources/MistDemoKit/Resources/js/app.js | 571 ++++++++ .../MistDemoKit/Resources/js/assets.js | 71 + .../Sources/MistDemoKit/Resources/js/auth.js | 148 ++ .../Sources/MistDemoKit/Resources/js/mode.js | 62 + .../MistDemoKit/Resources/js/pending.js | 28 + .../MistDemoKit/Resources/js/records.js | 120 ++ .../MistDemoKit/Resources/js/subscriptions.js | 227 +++ .../MistDemoKit/Resources/js/tokens.js | 50 + .../Sources/MistDemoKit/Resources/js/users.js | 95 ++ .../Sources/MistDemoKit/Resources/js/zones.js | 135 ++ .../Sources/MistDemoKit/Resources/styles.css | 283 ++++ .../MistDemoKit/Server/WebBackend.swift | 17 + .../MistDemoKit/Server/WebIndexHTML.swift | 59 +- .../Server/WebRequests+Zones.swift | 69 + .../MistDemoKit/Server/WebRequests.swift | 2 +- .../MistDemoKit/Server/WebResponse.swift | 7 + .../Server/WebServer+Pending.swift | 173 +++ .../MistDemoKit/Server/WebServer+Zones.swift | 66 + .../MistDemoKit/Server/WebServer.swift | 40 + .../MistDemoKit/Utilities/PendingStub.swift | 66 + .../Server/MockBackend+Calls.swift | 75 + .../MistDemoTests/Server/MockBackend.swift | 45 +- .../Server/WebServerTests+Index.swift | 114 +- .../Server/WebServerTests+Zones.swift | 112 ++ .../Utilities/TestPlatform.swift | 13 +- 79 files changed, 6463 insertions(+), 1072 deletions(-) create mode 100644 Examples/MistDemo/Sources/MistDemoApp/Models/RecordZoneChangesSnapshot.swift create mode 100644 Examples/MistDemo/Sources/MistDemoApp/Models/ResolveResult.swift create mode 100644 Examples/MistDemo/Sources/MistDemoApp/Services/CloudKitStore+Assets.swift create mode 100644 Examples/MistDemo/Sources/MistDemoApp/Services/CloudKitStore+PushTokens.swift create mode 100644 Examples/MistDemo/Sources/MistDemoApp/Services/CloudKitStore+Records.swift create mode 100644 Examples/MistDemo/Sources/MistDemoApp/Services/CloudKitStore+Subscriptions.swift create mode 100644 Examples/MistDemo/Sources/MistDemoApp/Services/CloudKitStore+Users.swift create mode 100644 Examples/MistDemo/Sources/MistDemoApp/Services/CloudKitStore+Zones.swift create mode 100644 Examples/MistDemo/Sources/MistDemoApp/Services/PlatformAliases.swift create mode 100644 Examples/MistDemo/Sources/MistDemoApp/Services/PlatformApplicationDelegate+NSApplicationDelegate.swift create mode 100644 Examples/MistDemo/Sources/MistDemoApp/Services/PlatformApplicationDelegate+RemoteNotifications.swift create mode 100644 Examples/MistDemo/Sources/MistDemoApp/Services/PlatformApplicationDelegate+UIApplicationDelegate.swift create mode 100644 Examples/MistDemo/Sources/MistDemoApp/Services/PlatformApplicationDelegate+WKApplicationDelegate.swift create mode 100644 Examples/MistDemo/Sources/MistDemoApp/Services/PlatformApplicationDelegate.swift create mode 100644 Examples/MistDemo/Sources/MistDemoApp/Services/PushNotificationDelegate.swift create mode 100644 Examples/MistDemo/Sources/MistDemoApp/Services/PushTokenReceiver.swift create mode 100644 Examples/MistDemo/Sources/MistDemoApp/Services/RemoteNotificationRegistering.swift create mode 100644 Examples/MistDemo/Sources/MistDemoApp/Views/AssetsView.swift create mode 100644 Examples/MistDemo/Sources/MistDemoApp/Views/CompositionDisclosure.swift create mode 100644 Examples/MistDemo/Sources/MistDemoApp/Views/PushTokensView.swift create mode 100644 Examples/MistDemo/Sources/MistDemoApp/Views/RecordsView.swift create mode 100644 Examples/MistDemo/Sources/MistDemoApp/Views/SubscriptionsView.swift create mode 100644 Examples/MistDemo/Sources/MistDemoApp/Views/UsersView.swift create mode 100644 Examples/MistDemo/Sources/MistDemoApp/Views/View+DeleteSwipeAction.swift create mode 100644 Examples/MistDemo/Sources/MistDemoKit/Commands/CreateTokenCommand.swift create mode 100644 Examples/MistDemo/Sources/MistDemoKit/Commands/ListSubscriptionsCommand.swift create mode 100644 Examples/MistDemo/Sources/MistDemoKit/Commands/LookupSubscriptionCommand.swift create mode 100644 Examples/MistDemo/Sources/MistDemoKit/Commands/RegisterTokenCommand.swift create mode 100644 Examples/MistDemo/Sources/MistDemoKit/Commands/RereferenceAssetCommand.swift create mode 100644 Examples/MistDemo/Sources/MistDemoKit/Commands/ResolveCommand.swift create mode 100644 Examples/MistDemo/Sources/MistDemoKit/Configuration/CreateTokenConfig.swift create mode 100644 Examples/MistDemo/Sources/MistDemoKit/Configuration/ListSubscriptionsConfig.swift create mode 100644 Examples/MistDemo/Sources/MistDemoKit/Configuration/LookupSubscriptionConfig.swift create mode 100644 Examples/MistDemo/Sources/MistDemoKit/Configuration/RegisterTokenConfig.swift create mode 100644 Examples/MistDemo/Sources/MistDemoKit/Configuration/RereferenceAssetConfig.swift create mode 100644 Examples/MistDemo/Sources/MistDemoKit/Configuration/ResolveConfig.swift create mode 100644 Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/CreateTokenPhase.swift create mode 100644 Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/ListSubscriptionsPhase.swift create mode 100644 Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/LookupSubscriptionPhase.swift create mode 100644 Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/RegisterTokenPhase.swift create mode 100644 Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/RereferenceAssetPhase.swift create mode 100644 Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/ResolveRecordsPhase.swift create mode 100644 Examples/MistDemo/Sources/MistDemoKit/PushTokenStatus.swift create mode 100644 Examples/MistDemo/Sources/MistDemoKit/Resources/js/app.js create mode 100644 Examples/MistDemo/Sources/MistDemoKit/Resources/js/assets.js create mode 100644 Examples/MistDemo/Sources/MistDemoKit/Resources/js/auth.js create mode 100644 Examples/MistDemo/Sources/MistDemoKit/Resources/js/mode.js create mode 100644 Examples/MistDemo/Sources/MistDemoKit/Resources/js/pending.js create mode 100644 Examples/MistDemo/Sources/MistDemoKit/Resources/js/records.js create mode 100644 Examples/MistDemo/Sources/MistDemoKit/Resources/js/subscriptions.js create mode 100644 Examples/MistDemo/Sources/MistDemoKit/Resources/js/tokens.js create mode 100644 Examples/MistDemo/Sources/MistDemoKit/Resources/js/users.js create mode 100644 Examples/MistDemo/Sources/MistDemoKit/Resources/js/zones.js create mode 100644 Examples/MistDemo/Sources/MistDemoKit/Resources/styles.css create mode 100644 Examples/MistDemo/Sources/MistDemoKit/Server/WebRequests+Zones.swift create mode 100644 Examples/MistDemo/Sources/MistDemoKit/Server/WebServer+Pending.swift create mode 100644 Examples/MistDemo/Sources/MistDemoKit/Server/WebServer+Zones.swift create mode 100644 Examples/MistDemo/Sources/MistDemoKit/Utilities/PendingStub.swift create mode 100644 Examples/MistDemo/Tests/MistDemoTests/Server/MockBackend+Calls.swift create mode 100644 Examples/MistDemo/Tests/MistDemoTests/Server/WebServerTests+Zones.swift diff --git a/Examples/MistDemo/.swiftlint.yml b/Examples/MistDemo/.swiftlint.yml index c33a1a85..4b110936 100644 --- a/Examples/MistDemo/.swiftlint.yml +++ b/Examples/MistDemo/.swiftlint.yml @@ -126,6 +126,10 @@ indentation_width: file_name: severity: error excluded: + # Holds only the per-platform `#if`-selected typealiases; named neutrally + # so no alias is misread as a `main_type` by `file_types_order` (see the + # file's header comment). The resulting `file_name` mismatch is expected. + - PlatformAliases.swift - Package.swift - AsyncHelpers.swift - UserInfoTestExtension.swift diff --git a/Examples/MistDemo/App/MistDemoApp.swift b/Examples/MistDemo/App/MistDemoApp.swift index 564b1f2f..740c7ce7 100644 --- a/Examples/MistDemo/App/MistDemoApp.swift +++ b/Examples/MistDemo/App/MistDemoApp.swift @@ -32,4 +32,9 @@ internal import SwiftUI @main internal struct MistDemoAppMain: AppMain { + // SwiftUI owns the AppDelegate's lifecycle via this adaptor; the + // delegate's static-weak `receiver` is wired from `CloudKitStore.init` + // so the OS-delivered APNs token lands back in the observable store. + @PlatformApplicationDelegateAdaptor(PushNotificationDelegate.self) + private var pushDelegate } diff --git a/Examples/MistDemo/MistDemoApp.entitlements b/Examples/MistDemo/MistDemoApp.entitlements index 66b80d9b..4911fde2 100644 --- a/Examples/MistDemo/MistDemoApp.entitlements +++ b/Examples/MistDemo/MistDemoApp.entitlements @@ -14,5 +14,9 @@ com.apple.security.network.client + aps-environment + development + com.apple.developer.aps-environment + development diff --git a/Examples/MistDemo/Package.swift b/Examples/MistDemo/Package.swift index 9a488def..04c9daa1 100644 --- a/Examples/MistDemo/Package.swift +++ b/Examples/MistDemo/Package.swift @@ -156,6 +156,8 @@ let package = Package( ], resources: [ .copy("Resources/index.html"), + .copy("Resources/styles.css"), + .copy("Resources/js"), ], swiftSettings: swiftSettings ), diff --git a/Examples/MistDemo/Sources/MistDemoApp/Models/RecordZoneChangesSnapshot.swift b/Examples/MistDemo/Sources/MistDemoApp/Models/RecordZoneChangesSnapshot.swift new file mode 100644 index 00000000..31e1a8cd --- /dev/null +++ b/Examples/MistDemo/Sources/MistDemoApp/Models/RecordZoneChangesSnapshot.swift @@ -0,0 +1,42 @@ +// +// RecordZoneChangesSnapshot.swift +// MistDemo +// +// Created by Leo Dion. +// Copyright © 2026 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +#if canImport(CloudKit) + internal import CloudKit + internal import Foundation + + /// Snapshot from `CKFetchRecordZoneChangesOperation` so the UI can show + /// what changed since the last sync token. + internal struct RecordZoneChangesSnapshot: Sendable { + internal let changedRecordNames: [String] + internal let deletedRecordNames: [String] + internal let serverChangeToken: CKServerChangeToken? + internal let moreComing: Bool + } +#endif diff --git a/Examples/MistDemo/Sources/MistDemoApp/Models/ResolveResult.swift b/Examples/MistDemo/Sources/MistDemoApp/Models/ResolveResult.swift new file mode 100644 index 00000000..8c580220 --- /dev/null +++ b/Examples/MistDemo/Sources/MistDemoApp/Models/ResolveResult.swift @@ -0,0 +1,47 @@ +// +// ResolveResult.swift +// MistDemo +// +// Created by Leo Dion. +// Copyright © 2026 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +#if canImport(CloudKit) + internal import Foundation + + /// Result of a composed `records/resolve`. The REST endpoint takes + /// either a record name or a share URL; the native CloudKit surface + /// branches on the input shape, so we record which branch ran. + internal struct ResolveResult: Sendable { + internal enum Source: String, Sendable { + case recordName + case shareURL + } + + internal let source: Source + internal let recordName: String? + internal let recordType: String? + internal let shareTitle: String? + } +#endif diff --git a/Examples/MistDemo/Sources/MistDemoApp/Services/CloudKitStore+Assets.swift b/Examples/MistDemo/Sources/MistDemoApp/Services/CloudKitStore+Assets.swift new file mode 100644 index 00000000..927cb4a0 --- /dev/null +++ b/Examples/MistDemo/Sources/MistDemoApp/Services/CloudKitStore+Assets.swift @@ -0,0 +1,90 @@ +// +// CloudKitStore+Assets.swift +// MistDemo +// +// Created by Leo Dion. +// Copyright © 2026 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +#if canImport(CloudKit) + internal import CloudKit + internal import Foundation + internal import MistDemoKit + + /// Result of a composed `assets/rereference`. The native CloudKit surface + /// can't move asset metadata between records in one call — we fetch the + /// source record, reuse its `CKAsset`, then save it onto the target + /// record. The result records both the source and target. + internal struct RereferenceResult: Sendable { + internal let sourceRecordName: String + internal let assetField: String + internal let targetRecordName: String + internal let targetAssetField: String + } + + extension CloudKitStore { + /// Upload `fileURL` as the `image` asset on a new Note record. Maps to + /// `assets/upload` in the REST surface — native CloudKit does the + /// upload inline as part of `database.save(_:)`. + internal func uploadAssetNote( + title: String, + index: Int64, + fileURL: URL + ) async throws -> Note { + try await createNote(title: title, index: index, imageURL: fileURL) + } + + /// Re-reference an asset from one record onto another. Composed call: + /// fetch the source record, pull its `CKAsset`, save the target with + /// that same asset. Native CloudKit doesn't expose a single-call + /// equivalent of the REST `assets/rereference` endpoint, hence the + /// composition. + internal func rereferenceAsset( + sourceRecordName: String, + assetField: String, + targetRecordName: String, + targetAssetField: String? = nil + ) async throws -> RereferenceResult { + let resolvedTargetField = targetAssetField ?? assetField + + let sourceID = CKRecord.ID(recordName: sourceRecordName) + let sourceRecord = try await database.record(for: sourceID) + guard let asset = sourceRecord[assetField] as? CKAsset else { + throw CloudKitStoreError.unexpectedSaveResult + } + + let targetID = CKRecord.ID(recordName: targetRecordName) + let targetRecord = try await database.record(for: targetID) + targetRecord[resolvedTargetField] = asset + _ = try await database.save(targetRecord) + + return RereferenceResult( + sourceRecordName: sourceRecordName, + assetField: assetField, + targetRecordName: targetRecordName, + targetAssetField: resolvedTargetField + ) + } + } +#endif diff --git a/Examples/MistDemo/Sources/MistDemoApp/Services/CloudKitStore+PushTokens.swift b/Examples/MistDemo/Sources/MistDemoApp/Services/CloudKitStore+PushTokens.swift new file mode 100644 index 00000000..8ccc73d1 --- /dev/null +++ b/Examples/MistDemo/Sources/MistDemoApp/Services/CloudKitStore+PushTokens.swift @@ -0,0 +1,94 @@ +// +// CloudKitStore+PushTokens.swift +// MistDemo +// +// Created by Leo Dion. +// Copyright © 2026 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +#if canImport(CloudKit) + internal import CloudKit + public import Foundation + internal import MistDemoKit + + #if canImport(AppKit) && !targetEnvironment(macCatalyst) + internal import AppKit + #elseif canImport(UIKit) && !os(watchOS) + internal import UIKit + #endif + + extension CloudKitStore { + /// Trigger APNs registration. Returns immediately after asking the OS; + /// the actual token arrives via the platform app delegate hook, which + /// the demo app forwards back into the store via `recordDeviceToken`. + /// On platforms where APNs isn't available we report `.failed`. + internal func requestPushNotificationRegistration() { + PlatformApplication.registerSharedForRemoteNotifications() + pushTokenStatus = .requesting + // #else + // pushTokenStatus = .failed( + // message: "APNs registration is unavailable on this platform." + // ) + // #endif + } + + /// Forward the APNs device token captured by the platform app delegate. + internal func recordDeviceToken(_ data: Data) { + let hex = data.map { String(format: "%02x", $0) }.joined() + pushTokenStatus = .registered(hexToken: hex) + } + + /// Forward the APNs registration error captured by the platform app delegate. + /// Surfaces the underlying NSError domain + code alongside the localized + /// message — the default `localizedDescription` for sandboxed-app / + /// signing failures collapses to "the operation couldn't be completed. + /// (OSStatus error N)" which by itself doesn't say what failed. + internal func recordDeviceTokenError(_ error: any Error) { + let nsError = error as NSError + let summary = + "\(error.localizedDescription)\n" + + "[\(nsError.domain) code \(nsError.code)]" + pushTokenStatus = .failed(message: summary) + } + } + + extension CloudKitStore: PushTokenReceiver { + /// Records the APNs device token forwarded by the platform app delegate. + public func didRegisterForRemoteNotifications(deviceToken: Data) { + recordDeviceToken(deviceToken) + } + + /// Records the APNs registration failure forwarded by the platform app + /// delegate. + public func didFailToRegisterForRemoteNotifications(error: any Error) { + recordDeviceTokenError(error) + } + + /// Stores a description of the most recent remote notification payload. + public func didReceiveRemoteNotification(userInfo: [AnyHashable: Any]) { + lastReceivedNotification = String(describing: userInfo) + } + } + +#endif diff --git a/Examples/MistDemo/Sources/MistDemoApp/Services/CloudKitStore+Records.swift b/Examples/MistDemo/Sources/MistDemoApp/Services/CloudKitStore+Records.swift new file mode 100644 index 00000000..15a01eeb --- /dev/null +++ b/Examples/MistDemo/Sources/MistDemoApp/Services/CloudKitStore+Records.swift @@ -0,0 +1,130 @@ +// +// CloudKitStore+Records.swift +// MistDemo +// +// Created by Leo Dion. +// Copyright © 2026 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +#if canImport(CloudKit) + internal import CloudKit + internal import Foundation + internal import MistDemoKit + + extension CloudKitStore { + /// Look up records by name. Maps to `records/lookup` in the REST + /// surface; uses `database.record(for:)` per ID. + internal func lookupRecords(names: [String]) async throws -> [Note] { + var notes: [Note] = [] + for name in names { + let recordID = CKRecord.ID(recordName: name) + let record = try await database.record(for: recordID) + if let note = Note(record) { + notes.append(note) + } + } + return notes + } + + /// Fetch record-level deltas for the given zone since `previousToken`. + /// Returns the changed and deleted record names plus the new sync + /// token. Pass that token back on the next call for an incremental + /// fetch. Maps to `records/changes` in the REST surface. + internal func fetchRecordZoneChanges( + zoneID: CKRecordZone.ID, + since previousToken: CKServerChangeToken? = nil + ) async throws -> RecordZoneChangesSnapshot { + try await withCheckedThrowingContinuation { continuation in + let configuration = + CKFetchRecordZoneChangesOperation + .ZoneConfiguration(previousServerChangeToken: previousToken) + let operation = CKFetchRecordZoneChangesOperation( + recordZoneIDs: [zoneID], + configurationsByRecordZoneID: [zoneID: configuration] + ) + + var changed: [String] = [] + var deleted: [String] = [] + var resolvedToken: CKServerChangeToken? + var moreComing = false + + operation.recordWasChangedBlock = { recordID, _ in + changed.append(recordID.recordName) + } + operation.recordWithIDWasDeletedBlock = { recordID, _ in + deleted.append(recordID.recordName) + } + operation.recordZoneFetchResultBlock = { _, result in + if case .success(let payload) = result { + resolvedToken = payload.serverChangeToken + moreComing = payload.moreComing + } + } + operation.fetchRecordZoneChangesResultBlock = { result in + switch result { + case .failure(let error): + continuation.resume(throwing: error) + case .success: + continuation.resume( + returning: RecordZoneChangesSnapshot( + changedRecordNames: changed, + deletedRecordNames: deleted, + serverChangeToken: resolvedToken, + moreComing: moreComing + ) + ) + } + } + + database.add(operation) + } + } + + /// Resolve a record reference. Accepts either a CloudKit record name + /// (routed through `database.record(for:)`) or a share URL (routed + /// through `CKContainer.share(metadataFor:)`). Maps to `records/resolve` + /// in the REST surface as a composed call. + internal func resolveReference(input: String) async throws -> ResolveResult { + if let url = URL(string: input), url.scheme?.hasPrefix("http") == true { + let container = CKContainer(identifier: containerIdentifier) + let metadata = try await container.shareMetadata(for: url) + return ResolveResult( + source: .shareURL, + recordName: metadata.rootRecordID.recordName, + recordType: metadata.rootRecord?.recordType, + shareTitle: metadata.share[CKShare.SystemFieldKey.title] as? String + ) + } + let record = try await database.record( + for: CKRecord.ID(recordName: input) + ) + return ResolveResult( + source: .recordName, + recordName: record.recordID.recordName, + recordType: record.recordType, + shareTitle: nil + ) + } + } +#endif diff --git a/Examples/MistDemo/Sources/MistDemoApp/Services/CloudKitStore+Subscriptions.swift b/Examples/MistDemo/Sources/MistDemoApp/Services/CloudKitStore+Subscriptions.swift new file mode 100644 index 00000000..6c74dcd3 --- /dev/null +++ b/Examples/MistDemo/Sources/MistDemoApp/Services/CloudKitStore+Subscriptions.swift @@ -0,0 +1,115 @@ +// +// CloudKitStore+Subscriptions.swift +// MistDemo +// +// Created by Leo Dion. +// Copyright © 2026 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +#if canImport(CloudKit) + internal import CloudKit + internal import Foundation + internal import MistDemoKit + + /// Display-friendly snapshot of a CKSubscription. + internal struct SubscriptionRow: Identifiable, Hashable, Sendable { + internal let id: String + internal let kind: String + internal let recordType: String? + + internal init(_ subscription: CKSubscription) { + self.id = subscription.subscriptionID + switch subscription { + case let query as CKQuerySubscription: + self.kind = "Query" + self.recordType = query.recordType + case let zone as CKRecordZoneSubscription: + self.kind = "Zone (\(zone.zoneID.zoneName))" + self.recordType = nil + case is CKDatabaseSubscription: + self.kind = "Database" + self.recordType = nil + default: + self.kind = "Other" + self.recordType = nil + } + } + } + + extension CloudKitStore { + /// List every CloudKit subscription registered on the selected + /// database. Maps to `subscriptions/list` in the REST surface. + internal func loadSubscriptions() async throws -> [SubscriptionRow] { + let subscriptions = try await database.allSubscriptions() + return subscriptions.map(SubscriptionRow.init).sorted { $0.id < $1.id } + } + + /// Look up specific subscriptions by ID. Maps to `subscriptions/lookup`. + internal func lookupSubscriptions( + ids: [String] + ) async throws -> [SubscriptionRow] { + try await withCheckedThrowingContinuation { continuation in + let operation = CKFetchSubscriptionsOperation(subscriptionIDs: ids) + var rows: [SubscriptionRow] = [] + operation.perSubscriptionResultBlock = { _, result in + if case .success(let subscription) = result { + rows.append(SubscriptionRow(subscription)) + } + } + operation.fetchSubscriptionsResultBlock = { result in + switch result { + case .success: + continuation.resume(returning: rows.sorted { $0.id < $1.id }) + case .failure(let error): + continuation.resume(throwing: error) + } + } + database.add(operation) + } + } + + /// Create a demo Note-query subscription so the subscriptions list has + /// something visible. Uses a fixed `subscriptionID` so repeated taps are + /// idempotent — CloudKit returns a conflict if it already exists, which + /// the UI surfaces via the standard error path. + internal func createDemoSubscription() async throws -> SubscriptionRow { + let subscription = CKQuerySubscription( + recordType: Note.recordType, + predicate: NSPredicate(value: true), + subscriptionID: "MistDemo.noteCreated", + options: [.firesOnRecordCreation] + ) + let info = CKSubscription.NotificationInfo() + info.shouldSendContentAvailable = true + subscription.notificationInfo = info + let saved = try await database.save(subscription) + return SubscriptionRow(saved) + } + + /// Delete a subscription by ID. + internal func deleteSubscription(id: String) async throws { + _ = try await database.deleteSubscription(withID: id) + } + } +#endif diff --git a/Examples/MistDemo/Sources/MistDemoApp/Services/CloudKitStore+Users.swift b/Examples/MistDemo/Sources/MistDemoApp/Services/CloudKitStore+Users.swift new file mode 100644 index 00000000..7f3145f1 --- /dev/null +++ b/Examples/MistDemo/Sources/MistDemoApp/Services/CloudKitStore+Users.swift @@ -0,0 +1,123 @@ +// +// CloudKitStore+Users.swift +// MistDemo +// +// Created by Leo Dion. +// Copyright © 2026 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +#if canImport(CloudKit) + internal import CloudKit + internal import Foundation + internal import MistDemoKit + + /// Display-friendly snapshot of a CKUserIdentity. + internal struct UserIdentityRow: Identifiable, Hashable, Sendable { + internal let id: String + internal let displayName: String? + internal let recordName: String? + internal let lookupHint: String? + + internal init(_ identity: CKUserIdentity, lookupHint: String? = nil) { + let recordName = identity.userRecordID?.recordName + self.id = + recordName + ?? identity.lookupInfo?.emailAddress + ?? identity.lookupInfo?.phoneNumber + ?? lookupHint + ?? UUID().uuidString + self.recordName = recordName + self.displayName = Self.formatName(identity.nameComponents) + self.lookupHint = lookupHint + } + + private static func formatName( + _ components: PersonNameComponents? + ) -> String? { + guard let components else { + return nil + } + let formatter = PersonNameComponentsFormatter() + let formatted = formatter.string(from: components) + return formatted.isEmpty ? nil : formatted + } + } + + extension CloudKitStore { + /// Look up a user identity by iCloud email address. Maps to + /// `users/lookup/email` in the REST surface; uses CloudKit's + /// `discoverUserIdentity(withEmailAddress:)`. + /// + /// `discoverUserIdentity(withEmailAddress:)` is deprecated as of macOS + /// 14 / iOS 17 but still ships on the supported platforms — the + /// CloudKit framework hasn't published an async replacement, so the + /// completion-handler form is wrapped via `withCheckedThrowingContinuation`. + internal func lookupUser(byEmail email: String) async throws -> UserIdentityRow? { + let container = CKContainer(identifier: containerIdentifier) + let identity: CKUserIdentity? = try await withCheckedThrowingContinuation { + continuation in + container.discoverUserIdentity(withEmailAddress: email) { + identity, error in + if let error { + continuation.resume(throwing: error) + } else { + continuation.resume(returning: identity) + } + } + } + return identity.map { UserIdentityRow($0, lookupHint: email) } + } + + /// Look up a user identity by record name. Maps to `users/lookup/id`. + internal func lookupUser(byRecordName recordName: String) async throws -> UserIdentityRow? { + let container = CKContainer(identifier: containerIdentifier) + let recordID = CKRecord.ID(recordName: recordName) + let identity: CKUserIdentity? = try await withCheckedThrowingContinuation { + continuation in + container.discoverUserIdentity(withUserRecordID: recordID) { + identity, error in + if let error { + continuation.resume(throwing: error) + } else { + continuation.resume(returning: identity) + } + } + } + return identity.map { UserIdentityRow($0, lookupHint: recordName) } + } + + /// Discover identities for a batch of email addresses, looping the + /// per-call API since the framework doesn't expose a batch entry point. + /// Maps to `users/discover` (POST) in the REST surface. + internal func discoverUsers(byEmails emails: [String]) async throws -> [UserIdentityRow] { + var rows: [UserIdentityRow] = [] + for email in emails { + if let row = try await lookupUser(byEmail: email) { + rows.append(row) + } + } + return rows + } + } +#endif diff --git a/Examples/MistDemo/Sources/MistDemoApp/Services/CloudKitStore+Zones.swift b/Examples/MistDemo/Sources/MistDemoApp/Services/CloudKitStore+Zones.swift new file mode 100644 index 00000000..31a485ec --- /dev/null +++ b/Examples/MistDemo/Sources/MistDemoApp/Services/CloudKitStore+Zones.swift @@ -0,0 +1,99 @@ +// +// CloudKitStore+Zones.swift +// MistDemo +// +// Created by Leo Dion. +// Copyright © 2026 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +#if canImport(CloudKit) + internal import CloudKit + internal import Foundation + internal import MistDemoKit + + /// Snapshot returned by `CKFetchDatabaseChangesOperation` so the UI can + /// show what changed since the last sync token. + internal struct DatabaseChangesSnapshot: Sendable { + internal let changedZoneIDs: [CKRecordZone.ID] + internal let deletedZoneIDs: [CKRecordZone.ID] + internal let serverChangeToken: CKServerChangeToken? + internal let moreComing: Bool + } + + extension CloudKitStore { + /// Create a new custom record zone in the selected database. Public + /// databases reject this — `CKModifyRecordZonesOperation` returns an + /// error which we surface to the UI. + internal func createZone(named name: String) async throws -> ZoneRow { + let zone = CKRecordZone(zoneName: name) + let saved = try await database.save(zone) + return ZoneRow(saved) + } + + /// Delete a custom record zone by name in the selected database. + internal func deleteZone(named name: String) async throws { + _ = try await database.deleteRecordZone( + withID: CKRecordZone.ID( + zoneName: name, ownerName: CKCurrentUserDefaultName + ) + ) + } + + /// Fetch database-scope changes since `previousToken`. Returns the + /// changed/deleted zone IDs plus the new sync token. Pass the returned + /// token on the next call to receive a delta. + internal func fetchDatabaseChanges( + since previousToken: CKServerChangeToken? = nil + ) async throws -> DatabaseChangesSnapshot { + try await withCheckedThrowingContinuation { continuation in + let operation = CKFetchDatabaseChangesOperation( + previousServerChangeToken: previousToken + ) + + var changed: [CKRecordZone.ID] = [] + var deleted: [CKRecordZone.ID] = [] + + operation.recordZoneWithIDWasDeletedBlock = { deleted.append($0) } + operation.recordZoneWithIDChangedBlock = { changed.append($0) } + operation.fetchDatabaseChangesResultBlock = { result in + switch result { + case .failure(let error): + continuation.resume(throwing: error) + case .success(let payload): + continuation.resume( + returning: DatabaseChangesSnapshot( + changedZoneIDs: changed, + deletedZoneIDs: deleted, + serverChangeToken: payload.serverChangeToken, + moreComing: payload.moreComing + ) + ) + } + } + + database.add(operation) + } + } + } +#endif diff --git a/Examples/MistDemo/Sources/MistDemoApp/Services/CloudKitStore.swift b/Examples/MistDemo/Sources/MistDemoApp/Services/CloudKitStore.swift index 2c448d81..0f450563 100644 --- a/Examples/MistDemo/Sources/MistDemoApp/Services/CloudKitStore.swift +++ b/Examples/MistDemo/Sources/MistDemoApp/Services/CloudKitStore.swift @@ -48,6 +48,15 @@ internal var lastError: String? internal var databaseScope: CKDatabase.Scope = .private + /// Latest APNs registration state. Driven by + /// `CloudKitStore+PushTokens.swift`; the SwiftUI Push Tokens view + /// observes this for live status updates. + internal var pushTokenStatus: PushTokenStatus = .idle + + /// Pretty-printed payload of the most recent remote notification + /// delivered while the app was running. Cleared on launch. + internal var lastReceivedNotification: String? + /// The signed-in iCloud user's record name. Mirrors `currentUserRecordName` /// in the web demo and is used to flag the "You" badge on notes the /// current user created. @@ -64,6 +73,10 @@ public init(containerIdentifier: String) { self.containerIdentifier = containerIdentifier self.container = CKContainer(identifier: containerIdentifier) + + // The platform AppDelegate is owned by SwiftUI; weak-link this store + // as the sink for its APNs callbacks. + PushNotificationDelegate.receiver = self } /// Apply the editable fields onto a CKRecord. CloudKit's system metadata diff --git a/Examples/MistDemo/Sources/MistDemoApp/Services/PlatformAliases.swift b/Examples/MistDemo/Sources/MistDemoApp/Services/PlatformAliases.swift new file mode 100644 index 00000000..2f0fb2db --- /dev/null +++ b/Examples/MistDemo/Sources/MistDemoApp/Services/PlatformAliases.swift @@ -0,0 +1,84 @@ +// +// PlatformAliases.swift +// MistDemo +// +// Created by Leo Dion. +// Copyright © 2026 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +// The per-platform aliases live together (rather than one type per file) +// because each is a thin, `#if`-selected typealias. The file is deliberately +// not named after any one alias — in project-mode lint SwiftLint parses every +// `#if` branch, so a filename-matching typealias ladder would be misread as a +// `main_type` sitting amongst `supporting_type`s (`file_types_order`). Naming +// the file neutrally keeps every alias a plain supporting type; the resulting +// `file_name` mismatch is waived for this file in `.swiftlint.yml`. +#if canImport(SwiftUI) + public import SwiftUI + + #if canImport(AppKit) && !targetEnvironment(macCatalyst) + public import AppKit + + /// The platform application type (`NSApplication` on AppKit, + /// `UIApplication` on UIKit, `WKApplication` on watchOS). Lets the + /// push-notification delegate and registration call sites name one type + /// instead of branching. + public typealias PlatformApplication = NSApplication + + /// The platform application delegate protocol matching + /// ``PlatformApplication``. + public typealias ApplicationDelegate = NSApplicationDelegate + + /// SwiftUI's delegate-adaptor property wrapper matching + /// ``ApplicationDelegate``. Lets `@main` declare the adaptor once + /// rather than under parallel `#if` branches. + public typealias PlatformApplicationDelegateAdaptor = NSApplicationDelegateAdaptor + #elseif canImport(WatchKit) + public import WatchKit + + /// The platform application type. See the AppKit branch for full notes. + public typealias PlatformApplication = WKApplication + + /// The platform application delegate protocol matching + /// ``PlatformApplication``. + public typealias ApplicationDelegate = WKApplicationDelegate + + /// SwiftUI's delegate-adaptor property wrapper matching + /// ``ApplicationDelegate``. + public typealias PlatformApplicationDelegateAdaptor = WKApplicationDelegateAdaptor + #elseif canImport(UIKit) + public import UIKit + + /// The platform application type. See the AppKit branch for full notes. + public typealias PlatformApplication = UIApplication + + /// The platform application delegate protocol matching + /// ``PlatformApplication``. + public typealias ApplicationDelegate = UIApplicationDelegate + + /// SwiftUI's delegate-adaptor property wrapper matching + /// ``ApplicationDelegate``. + public typealias PlatformApplicationDelegateAdaptor = UIApplicationDelegateAdaptor + #endif +#endif diff --git a/Examples/MistDemo/Sources/MistDemoApp/Services/PlatformApplicationDelegate+NSApplicationDelegate.swift b/Examples/MistDemo/Sources/MistDemoApp/Services/PlatformApplicationDelegate+NSApplicationDelegate.swift new file mode 100644 index 00000000..13d97204 --- /dev/null +++ b/Examples/MistDemo/Sources/MistDemoApp/Services/PlatformApplicationDelegate+NSApplicationDelegate.swift @@ -0,0 +1,43 @@ +// +// PlatformApplicationDelegate+NSApplicationDelegate.swift +// MistDemo +// +// Created by Leo Dion. +// Copyright © 2026 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +#if canImport(AppKit) && !targetEnvironment(macCatalyst) + public import AppKit + internal import Foundation + + extension PlatformApplicationDelegate where Self: NSApplicationDelegate { + /// A remote notification arrived (AppKit variant). + public func application( + _ application: NSApplication, + didReceiveRemoteNotification userInfo: [String: Any] + ) { + Self.receiver?.didReceiveRemoteNotification(userInfo: userInfo) + } + } +#endif diff --git a/Examples/MistDemo/Sources/MistDemoApp/Services/PlatformApplicationDelegate+RemoteNotifications.swift b/Examples/MistDemo/Sources/MistDemoApp/Services/PlatformApplicationDelegate+RemoteNotifications.swift new file mode 100644 index 00000000..41ab06c9 --- /dev/null +++ b/Examples/MistDemo/Sources/MistDemoApp/Services/PlatformApplicationDelegate+RemoteNotifications.swift @@ -0,0 +1,59 @@ +// +// PlatformApplicationDelegate+RemoteNotifications.swift +// MistDemo +// +// Created by Leo Dion. +// Copyright © 2026 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +// The `application(_:didRegister…)` / `didFail…` selectors are identical on +// AppKit and UIKit (both take the unified ``PlatformApplication``), so they +// share one extension here. watchOS's `WKApplicationDelegate` uses +// parameter-less selectors, so its variants live in +// `PlatformApplicationDelegate+WKApplicationDelegate.swift` instead. +#if (canImport(AppKit) && !targetEnvironment(macCatalyst)) || (canImport(UIKit) && !os(watchOS)) + public import Foundation + + extension PlatformApplicationDelegate { + /// APNs delivered a device token — forward it to the registered receiver. + /// + /// `public` because, when adopted by a `public` class, this satisfies the + /// matching requirement in the `public` system delegate protocol. + public func application( + _ application: PlatformApplication, + didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data + ) { + Self.receiver?.didRegisterForRemoteNotifications(deviceToken: deviceToken) + } + + /// APNs refused registration — forward the error to the receiver so it + /// can surface in the UI. + public func application( + _ application: PlatformApplication, + didFailToRegisterForRemoteNotificationsWithError error: any Error + ) { + Self.receiver?.didFailToRegisterForRemoteNotifications(error: error) + } + } +#endif diff --git a/Examples/MistDemo/Sources/MistDemoApp/Services/PlatformApplicationDelegate+UIApplicationDelegate.swift b/Examples/MistDemo/Sources/MistDemoApp/Services/PlatformApplicationDelegate+UIApplicationDelegate.swift new file mode 100644 index 00000000..d1fe3a69 --- /dev/null +++ b/Examples/MistDemo/Sources/MistDemoApp/Services/PlatformApplicationDelegate+UIApplicationDelegate.swift @@ -0,0 +1,45 @@ +// +// PlatformApplicationDelegate+UIApplicationDelegate.swift +// MistDemo +// +// Created by Leo Dion. +// Copyright © 2026 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +#if canImport(UIKit) && !os(watchOS) + internal import Foundation + public import UIKit + + extension PlatformApplicationDelegate where Self: UIApplicationDelegate { + /// A remote notification arrived (UIKit variant). + public func application( + _ application: UIApplication, + didReceiveRemoteNotification userInfo: [AnyHashable: Any], + fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void + ) { + Self.receiver?.didReceiveRemoteNotification(userInfo: userInfo) + completionHandler(.newData) + } + } +#endif diff --git a/Examples/MistDemo/Sources/MistDemoApp/Services/PlatformApplicationDelegate+WKApplicationDelegate.swift b/Examples/MistDemo/Sources/MistDemoApp/Services/PlatformApplicationDelegate+WKApplicationDelegate.swift new file mode 100644 index 00000000..10fd5c30 --- /dev/null +++ b/Examples/MistDemo/Sources/MistDemoApp/Services/PlatformApplicationDelegate+WKApplicationDelegate.swift @@ -0,0 +1,54 @@ +// +// PlatformApplicationDelegate+WKApplicationDelegate.swift +// MistDemo +// +// Created by Leo Dion. +// Copyright © 2026 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +#if canImport(WatchKit) + internal import Foundation + public import WatchKit + + extension PlatformApplicationDelegate where Self: WKApplicationDelegate { + /// APNs delivered a device token — forward it to the registered receiver. + public func didRegisterForRemoteNotifications(withDeviceToken deviceToken: Data) { + Self.receiver?.didRegisterForRemoteNotifications(deviceToken: deviceToken) + } + + /// APNs refused registration — forward the error to the receiver. + public func didFailToRegisterForRemoteNotificationsWithError(_ error: any Error) { + Self.receiver?.didFailToRegisterForRemoteNotifications(error: error) + } + + /// A remote notification arrived (watchOS variant). + public func didReceiveRemoteNotification( + _ userInfo: [AnyHashable: Any], + fetchCompletionHandler completionHandler: @escaping (WKBackgroundFetchResult) -> Void + ) { + Self.receiver?.didReceiveRemoteNotification(userInfo: userInfo) + completionHandler(.newData) + } + } +#endif diff --git a/Examples/MistDemo/Sources/MistDemoApp/Services/PlatformApplicationDelegate.swift b/Examples/MistDemo/Sources/MistDemoApp/Services/PlatformApplicationDelegate.swift new file mode 100644 index 00000000..c16617d2 --- /dev/null +++ b/Examples/MistDemo/Sources/MistDemoApp/Services/PlatformApplicationDelegate.swift @@ -0,0 +1,48 @@ +// +// PlatformApplicationDelegate.swift +// MistDemo +// +// Created by Leo Dion. +// Copyright © 2026 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +// `@objc` requires the Objective-C runtime, which is absent on +// Linux/Windows. This delegate protocol exists only to bridge APNs callbacks +// on Apple platforms, so gate it on Objective-C interop availability. +#if canImport(ObjectiveC) + // `@objc` requires the Objective-C runtime, surfaced here via Foundation. + internal import Foundation + + /// App-level delegate behavior that funnels APNs callbacks to a + /// ``PushTokenReceiver``. Deliberately independent of the system delegate + /// protocol (``ApplicationDelegate``) and of the concrete + /// ``PushNotificationDelegate`` class — it only knows about the receiver. The + /// shared callbacks are supplied by the extensions below (and the + /// per-platform extension files), keyed off the static `receiver`. + @MainActor + @objc + public protocol PlatformApplicationDelegate: AnyObject { + static var receiver: (any PushTokenReceiver)? { get } + } +#endif diff --git a/Examples/MistDemo/Sources/MistDemoApp/Services/PushNotificationDelegate.swift b/Examples/MistDemo/Sources/MistDemoApp/Services/PushNotificationDelegate.swift new file mode 100644 index 00000000..92ff301b --- /dev/null +++ b/Examples/MistDemo/Sources/MistDemoApp/Services/PushNotificationDelegate.swift @@ -0,0 +1,69 @@ +// +// PushNotificationDelegate.swift +// MistDemo +// +// Created by Leo Dion. +// Copyright © 2026 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +// Conforms to the SwiftUI-defined ``ApplicationDelegate`` typealias and uses +// `NSObject`/`@objc` machinery, so gate on SwiftUI (which implies an Apple +// platform with Objective-C interop) to keep Linux/Windows builds clean. +#if canImport(SwiftUI) + public import Foundation + + #if canImport(AppKit) && !targetEnvironment(macCatalyst) + public import AppKit + #elseif canImport(WatchKit) + public import WatchKit + #elseif canImport(UIKit) && !os(watchOS) + public import UIKit + #endif + + /// Universal AppKit/UIKit/watchOS delegate that catches APNs callbacks + /// SwiftUI can't observe directly. SwiftUI's `@NSApplicationDelegateAdaptor` + /// / `@UIApplicationDelegateAdaptor` / `@WKApplicationDelegateAdaptor` + /// instantiates this with the parameterless `NSObject` init, so the receiver + /// is wired via a `static weak` set by `CloudKitStore.init` rather than + /// passed in. + /// + /// The APNs callbacks themselves are supplied by + /// ``PlatformApplicationDelegate`` and its per-platform extensions; + /// conforming to both that protocol and the system ``ApplicationDelegate`` + /// (required by the SwiftUI adaptor) is all this class needs to declare. + @MainActor + public final class PushNotificationDelegate: + NSObject, ApplicationDelegate, PlatformApplicationDelegate + { + /// The object the platform delegate forwards APNs callbacks to. Set by + /// `CloudKitStore.init`; held weakly so the store's lifetime governs it. + public static weak var receiver: (any PushTokenReceiver)? + + /// Required by SwiftUI's delegate adaptor, which constructs the + /// delegate with no arguments at app launch. + override public init() { + super.init() + } + } +#endif diff --git a/Examples/MistDemo/Sources/MistDemoApp/Services/PushTokenReceiver.swift b/Examples/MistDemo/Sources/MistDemoApp/Services/PushTokenReceiver.swift new file mode 100644 index 00000000..63916cf2 --- /dev/null +++ b/Examples/MistDemo/Sources/MistDemoApp/Services/PushTokenReceiver.swift @@ -0,0 +1,47 @@ +// +// PushTokenReceiver.swift +// MistDemo +// +// Created by Leo Dion. +// Copyright © 2026 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +// `@objc` requires the Objective-C runtime, which is absent on +// Linux/Windows. The push-notification bridge is Apple-only, so gate the +// whole protocol on Objective-C interop availability. +#if canImport(ObjectiveC) + public import Foundation + + /// Receiver side of the platform push-notification bridge. The + /// `PushNotificationDelegate` (AppKit / UIKit) forwards OS callbacks + /// to whatever object is currently registered as + /// `PushNotificationDelegate.receiver`. + @MainActor + @objc + public protocol PushTokenReceiver: AnyObject { + func didRegisterForRemoteNotifications(deviceToken: Data) + func didFailToRegisterForRemoteNotifications(error: any Error) + func didReceiveRemoteNotification(userInfo: [AnyHashable: Any]) + } +#endif diff --git a/Examples/MistDemo/Sources/MistDemoApp/Services/RemoteNotificationRegistering.swift b/Examples/MistDemo/Sources/MistDemoApp/Services/RemoteNotificationRegistering.swift new file mode 100644 index 00000000..d3ee04fc --- /dev/null +++ b/Examples/MistDemo/Sources/MistDemoApp/Services/RemoteNotificationRegistering.swift @@ -0,0 +1,65 @@ +// +// RemoteNotificationRegistering.swift +// MistDemo +// +// Created by Leo Dion. +// Copyright © 2026 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +#if canImport(SwiftUI) + public import SwiftUI + + /// Unifies the per-platform "register for remote notifications" entry point + /// so push code can call it through ``PlatformApplication`` without + /// branching. The accessor is named `sharedApplication` rather than `shared` + /// because `WKApplication.shared()` is a method, not a property — reusing the + /// `shared` name would collide with it. + @MainActor + internal protocol RemoteNotificationRegistering { + static var sharedApplication: PlatformApplication { get } + func registerForRemoteNotifications() + } + + extension RemoteNotificationRegistering { + /// Registers the shared platform application for remote notifications — + /// the single cross-platform entry point push code calls. + internal static func registerSharedForRemoteNotifications() { + sharedApplication.registerForRemoteNotifications() + } + } + + #if canImport(AppKit) && !targetEnvironment(macCatalyst) + extension NSApplication: RemoteNotificationRegistering { + internal static var sharedApplication: NSApplication { shared } + } + #elseif canImport(WatchKit) + extension WKApplication: RemoteNotificationRegistering { + internal static var sharedApplication: WKApplication { shared() } + } + #elseif canImport(UIKit) + extension UIApplication: RemoteNotificationRegistering { + internal static var sharedApplication: UIApplication { shared } + } + #endif +#endif diff --git a/Examples/MistDemo/Sources/MistDemoApp/Views/AssetsView.swift b/Examples/MistDemo/Sources/MistDemoApp/Views/AssetsView.swift new file mode 100644 index 00000000..85b45d40 --- /dev/null +++ b/Examples/MistDemo/Sources/MistDemoApp/Views/AssetsView.swift @@ -0,0 +1,158 @@ +// +// AssetsView.swift +// MistDemo +// +// Created by Leo Dion. +// Copyright © 2026 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +#if canImport(SwiftUI) && canImport(CloudKit) + internal import MistDemoKit + internal import SwiftUI + + /// View driving `assets/upload` and the composed `assets/rereference` + /// against native CloudKit. Upload is a one-step `database.save(_:)` + /// with a `CKAsset` payload; rereference fetches the source record, + /// reuses its asset descriptor, and saves the target — surfaced via + /// `CompositionDisclosure`. + internal struct AssetsView: View { + @Environment(CloudKitStore.self) private var service + @State private var uploadTitle: String = "Asset demo" + @State private var uploadIndex: Int64 = 0 + @State private var uploadFilePath: String = "" + @State private var uploadResult: Note? + @State private var uploadError: String? + @State private var sourceRecord: String = "" + @State private var assetField: String = "image" + @State private var targetRecord: String = "" + @State private var targetAssetField: String = "" + @State private var rereferenceResult: RereferenceResult? + @State private var rereferenceError: String? + + internal var body: some View { + Form { + uploadSection + rereferenceSection + } + .formStyle(.grouped) + .navigationTitle("Assets") + } + + private var uploadSection: some View { + Section { + TextField("Note title", text: $uploadTitle) + TextField( + "Sort index", value: $uploadIndex, format: .number + ) + TextField("Local file path", text: $uploadFilePath) + .font(.body.monospaced()) + Button("Upload") { Task { await runUpload() } } + .disabled(uploadTitle.isEmpty || uploadFilePath.isEmpty) + if let uploadError { + Text(uploadError).font(.callout).foregroundStyle(.red) + } + if let uploadResult { + LabeledContent("Created", value: uploadResult.id) + } + } header: { + Text("Upload — assets/upload") + } footer: { + Text( + "Creates a Note with the file at the given path as its " + + "`image` asset. Native CKAsset upload runs inline as part of " + + "database.save(_:)." + ) + .font(.caption) + } + } + + private var rereferenceSection: some View { + Section { + TextField("Source record name", text: $sourceRecord) + .font(.body.monospaced()) + TextField("Asset field name", text: $assetField) + TextField("Target record name", text: $targetRecord) + .font(.body.monospaced()) + TextField( + "Target asset field (optional)", + text: $targetAssetField + ) + Button("Rereference") { Task { await runRereference() } } + .disabled( + sourceRecord.isEmpty || targetRecord.isEmpty + || assetField.isEmpty + ) + if let rereferenceError { + Text(rereferenceError).font(.callout).foregroundStyle(.red) + } + if let result = rereferenceResult { + LabeledContent("Source", value: result.sourceRecordName) + LabeledContent("Target", value: result.targetRecordName) + LabeledContent("Field", value: result.targetAssetField) + } + CompositionDisclosure( + restEndpoint: "assets/rereference", + steps: [ + "database.record(for: source) — fetch source record", + "Read source[assetField] as CKAsset", + "database.record(for: target) — fetch target record", + "target[targetAssetField] = asset; database.save(target)", + ] + ) + } header: { + Text("Rereference — assets/rereference (composed)") + } + } + + private func runUpload() async { + uploadError = nil + uploadResult = nil + let url = URL(fileURLWithPath: uploadFilePath) + do { + uploadResult = try await service.uploadAssetNote( + title: uploadTitle, + index: uploadIndex, + fileURL: url + ) + } catch { + uploadError = error.localizedDescription + } + } + + private func runRereference() async { + rereferenceError = nil + rereferenceResult = nil + do { + rereferenceResult = try await service.rereferenceAsset( + sourceRecordName: sourceRecord, + assetField: assetField, + targetRecordName: targetRecord, + targetAssetField: targetAssetField.isEmpty ? nil : targetAssetField + ) + } catch { + rereferenceError = error.localizedDescription + } + } + } +#endif diff --git a/Examples/MistDemo/Sources/MistDemoApp/Views/CompositionDisclosure.swift b/Examples/MistDemo/Sources/MistDemoApp/Views/CompositionDisclosure.swift new file mode 100644 index 00000000..c052e5c3 --- /dev/null +++ b/Examples/MistDemo/Sources/MistDemoApp/Views/CompositionDisclosure.swift @@ -0,0 +1,80 @@ +// +// CompositionDisclosure.swift +// MistDemo +// +// Created by Leo Dion. +// Copyright © 2026 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +#if canImport(SwiftUI) + internal import SwiftUI + + /// Disclosure group documenting the underlying CloudKit operations that + /// compose a single REST endpoint. Used by `RecordsView` (`records/resolve`) + /// and `AssetsView` (`assets/rereference`) so users learning the SDK can + /// see where native CloudKit's API shape diverges from the REST surface. + internal struct CompositionDisclosure: View { + internal let restEndpoint: String + internal let steps: [String] + + internal var body: some View { + // `DisclosureGroup` is unavailable on watchOS and tvOS, so there the + // composition detail is shown inline (non-collapsible) under a plain + // header. + #if os(watchOS) || os(tvOS) + VStack(alignment: .leading, spacing: 6) { + Text("📎 Composition").font(.subheadline.bold()) + compositionDetail + } + #else + DisclosureGroup("📎 Composition") { + compositionDetail + } + .font(.subheadline) + #endif + } + + private var compositionDetail: some View { + VStack(alignment: .leading, spacing: 6) { + Text("REST endpoint: ").font(.caption.bold()) + + Text(restEndpoint).font(.caption.monospaced()) + Text("Underlying CloudKit operations:") + .font(.caption.bold()) + ForEach(Array(steps.enumerated()), id: \.offset) { index, step in + HStack(alignment: .top, spacing: 6) { + Text("\(index + 1).").font(.caption) + Text(step).font(.caption.monospaced()) + } + } + } + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.vertical, 4) + } + + internal init(restEndpoint: String, steps: [String]) { + self.restEndpoint = restEndpoint + self.steps = steps + } + } +#endif diff --git a/Examples/MistDemo/Sources/MistDemoApp/Views/DetailColumnRoot.swift b/Examples/MistDemo/Sources/MistDemoApp/Views/DetailColumnRoot.swift index 82684084..448fe89b 100644 --- a/Examples/MistDemo/Sources/MistDemoApp/Views/DetailColumnRoot.swift +++ b/Examples/MistDemo/Sources/MistDemoApp/Views/DetailColumnRoot.swift @@ -42,11 +42,21 @@ ZoneListView() case .query: QueryView() + case .records: + RecordsView() + case .subscriptions: + SubscriptionsView() + case .pushTokens: + PushTokensView() + case .assets: + AssetsView() + case .users: + UsersView() case nil: ContentUnavailableView( "Pick a section from the sidebar", systemImage: "sidebar.left", - description: Text("Account, Zones, or Query Records") + description: Text("Account, Zones, Records, Subscriptions, …") ) } } diff --git a/Examples/MistDemo/Sources/MistDemoApp/Views/PushTokensView.swift b/Examples/MistDemo/Sources/MistDemoApp/Views/PushTokensView.swift new file mode 100644 index 00000000..0fe57f3f --- /dev/null +++ b/Examples/MistDemo/Sources/MistDemoApp/Views/PushTokensView.swift @@ -0,0 +1,109 @@ +// +// PushTokensView.swift +// MistDemo +// +// Created by Leo Dion. +// Copyright © 2026 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +#if canImport(SwiftUI) && canImport(CloudKit) + internal import MistDemoKit + internal import SwiftUI + + /// Surfaces the device's APNs token for cross-reference with the REST + /// `tokens/create` / `tokens/register` workflows. The native demo uses + /// the CloudKit framework, so the token here is informational only: + /// iCloud routes pushes to the signed-in user's devices without anyone + /// passing the device token to CloudKit. See the Subscriptions view for + /// the actual server-side notification setup. + internal struct PushTokensView: View { + @Environment(CloudKitStore.self) private var service + + internal var body: some View { + Form { + Section { + tokenStatus + Button("Register for Remote Notifications") { + service.requestPushNotificationRegistration() + } + } header: { + Text("APNs device token") + } footer: { + Text( + "Real push delivery requires a signed build with the push " + + "entitlement and a paid developer account. Simulators and " + + "unentitled builds surface the registration error path " + + "below instead of a token. The native CloudKit framework " + + "binds this token to the signed-in iCloud account " + + "automatically — to actually receive a push, create a " + + "CKSubscription from the Subscriptions tab. The MistKit " + + "REST tokens/create + tokens/register endpoints exist for " + + "server-side scenarios where there's no iCloud user " + + "context to route through." + ) + .font(.caption) + } + if let payload = service.lastReceivedNotification { + Section { + Text(payload) + .font(.callout.monospaced()) + #if !os(tvOS) && !os(watchOS) + .textSelection(.enabled) + #endif + } header: { + Text("Last received remote notification") + } + } + } + .formStyle(.grouped) + .navigationTitle("Push Tokens") + } + + @ViewBuilder + private var tokenStatus: some View { + switch service.pushTokenStatus { + case .idle: + LabeledContent("APNs Token", value: "Not requested yet") + case .requesting: + HStack { + ProgressView().controlSize(.small) + Text("Requesting…") + } + case .registered(let hex): + LabeledContent("APNs Token") { + Text(hex) + .font(.callout.monospaced()) + .lineLimit(3) + .truncationMode(.middle) + #if !os(tvOS) && !os(watchOS) + .textSelection(.enabled) + #endif + } + case .failed(let message): + LabeledContent("APNs Token", value: "Failed") + Text(message).font(.caption).foregroundStyle(.red) + } + } + } +#endif diff --git a/Examples/MistDemo/Sources/MistDemoApp/Views/RecordsView.swift b/Examples/MistDemo/Sources/MistDemoApp/Views/RecordsView.swift new file mode 100644 index 00000000..d55bd707 --- /dev/null +++ b/Examples/MistDemo/Sources/MistDemoApp/Views/RecordsView.swift @@ -0,0 +1,189 @@ +// +// RecordsView.swift +// MistDemo +// +// Created by Leo Dion. +// Copyright © 2026 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +#if canImport(SwiftUI) && canImport(CloudKit) + internal import CloudKit + internal import MistDemoKit + internal import SwiftUI + + /// View driving `records/lookup`, `records/changes`, and the composed + /// `records/resolve` against native CloudKit. Lookup and resolve accept + /// the same input shape (a record name) but resolve also accepts a + /// share URL; the underlying composition is documented inline via + /// `CompositionDisclosure`. + internal struct RecordsView: View { + @Environment(CloudKitStore.self) private var service + @State private var lookupInput: String = "" + @State private var lookupResults: [Note] = [] + @State private var lookupError: String? + @State private var resolveInput: String = "" + @State private var resolveResult: ResolveResult? + @State private var resolveError: String? + @State private var changesSnapshot: RecordZoneChangesSnapshot? + @State private var changesError: String? + @State private var changesToken: CKServerChangeToken? + @State private var loading = false + + internal var body: some View { + Form { + lookupSection + changesSection + resolveSection + } + .formStyle(.grouped) + .navigationTitle("Records") + } + + private var lookupSection: some View { + Section { + TextField("Record name", text: $lookupInput) + .font(.body.monospaced()) + Button("Lookup") { Task { await runLookup() } } + .disabled(lookupInput.isEmpty || loading) + if let lookupError { + Text(lookupError).font(.callout).foregroundStyle(.red) + } + ForEach(lookupResults) { note in + VStack(alignment: .leading, spacing: 2) { + Text(note.title ?? "(untitled)").font(.headline) + Text(note.id).font(.caption.monospaced()) + .foregroundStyle(.secondary) + } + } + } header: { + Text("Lookup — records/lookup") + } footer: { + Text("CKFetchRecordsOperation / database.record(for:)") + .font(.caption) + } + } + + private var changesSection: some View { + Section { + Button("Fetch changes (_defaultZone)") { + Task { await runChanges() } + } + .disabled(loading) + if let changesError { + Text(changesError).font(.callout).foregroundStyle(.red) + } + if let snapshot = changesSnapshot { + LabeledContent("Changed", value: "\(snapshot.changedRecordNames.count)") + LabeledContent("Deleted", value: "\(snapshot.deletedRecordNames.count)") + LabeledContent("More coming", value: snapshot.moreComing ? "Yes" : "No") + } + } header: { + Text("Changes — records/changes") + } footer: { + Text("CKFetchRecordZoneChangesOperation. Repeat calls return deltas.") + .font(.caption) + } + } + + private var resolveSection: some View { + Section { + TextField( + "Record name or share URL", + text: $resolveInput + ) + .font(.body.monospaced()) + Button("Resolve") { Task { await runResolve() } } + .disabled(resolveInput.isEmpty || loading) + if let resolveError { + Text(resolveError).font(.callout).foregroundStyle(.red) + } + if let result = resolveResult { + LabeledContent("Branch", value: result.source.rawValue) + if let name = result.recordName { + LabeledContent("Record", value: name) + } + if let type = result.recordType { + LabeledContent("Type", value: type) + } + if let title = result.shareTitle { + LabeledContent("Share title", value: title) + } + } + CompositionDisclosure( + restEndpoint: "records/resolve", + steps: [ + "Record name → database.record(for: CKRecord.ID(recordName:))", + "Share URL → container.shareMetadata(for: url)", + ] + ) + } header: { + Text("Resolve — records/resolve (composed)") + } + } + + private func runLookup() async { + loading = true + lookupError = nil + defer { loading = false } + do { + let names = lookupInput.split(separator: ",") + .map { String($0).trimmingCharacters(in: .whitespaces) } + .filter { !$0.isEmpty } + lookupResults = try await service.lookupRecords(names: names) + } catch { + lookupResults = [] + lookupError = error.localizedDescription + } + } + + private func runChanges() async { + loading = true + changesError = nil + defer { loading = false } + do { + let snapshot = try await service.fetchRecordZoneChanges( + zoneID: CKRecordZone.ID(zoneName: CKRecordZone.ID.defaultZoneName), + since: changesToken + ) + changesSnapshot = snapshot + changesToken = snapshot.serverChangeToken + } catch { + changesSnapshot = nil + changesError = error.localizedDescription + } + } + + private func runResolve() async { + loading = true + resolveError = nil + defer { loading = false } + do { + resolveResult = try await service.resolveReference(input: resolveInput) + } catch { + resolveResult = nil + resolveError = error.localizedDescription + } + } + } +#endif diff --git a/Examples/MistDemo/Sources/MistDemoApp/Views/SidebarItem.swift b/Examples/MistDemo/Sources/MistDemoApp/Views/SidebarItem.swift index 2d3a12ee..f69ccb1e 100644 --- a/Examples/MistDemo/Sources/MistDemoApp/Views/SidebarItem.swift +++ b/Examples/MistDemo/Sources/MistDemoApp/Views/SidebarItem.swift @@ -32,12 +32,22 @@ internal enum SidebarItem: Hashable, CaseIterable { case account case zones case query + case records + case subscriptions + case pushTokens + case assets + case users internal var label: String { switch self { case .account: return "iCloud Account" case .zones: return "Zones" case .query: return "Query Records" + case .records: return "Records" + case .subscriptions: return "Subscriptions" + case .pushTokens: return "Push Tokens" + case .assets: return "Assets" + case .users: return "Users" } } @@ -46,6 +56,11 @@ internal enum SidebarItem: Hashable, CaseIterable { case .account: return "person.crop.circle" case .zones: return "tray.full" case .query: return "magnifyingglass" + case .records: return "doc.text" + case .subscriptions: return "bell" + case .pushTokens: return "key.radiowaves.forward" + case .assets: return "photo" + case .users: return "person.2" } } } diff --git a/Examples/MistDemo/Sources/MistDemoApp/Views/SubscriptionsView.swift b/Examples/MistDemo/Sources/MistDemoApp/Views/SubscriptionsView.swift new file mode 100644 index 00000000..08a3afba --- /dev/null +++ b/Examples/MistDemo/Sources/MistDemoApp/Views/SubscriptionsView.swift @@ -0,0 +1,156 @@ +// +// SubscriptionsView.swift +// MistDemo +// +// Created by Leo Dion. +// Copyright © 2026 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +#if canImport(SwiftUI) && canImport(CloudKit) + internal import MistDemoKit + internal import SwiftUI + + /// View driving `subscriptions/list` and `subscriptions/lookup` against + /// native CloudKit. Includes a "Create demo subscription" button so the + /// list has something to render against a fresh container. + internal struct SubscriptionsView: View { + @Environment(CloudKitStore.self) private var service + @State private var rows: [SubscriptionRow] = [] + @State private var loading = false + @State private var loadError: String? + @State private var lookupInput: String = "" + + internal var body: some View { + Group { + if loading { + ProgressView("Loading subscriptions…") + } else if let loadError { + ContentUnavailableView( + "Couldn't load subscriptions", + systemImage: "exclamationmark.triangle", + description: Text(loadError) + ) + } else if rows.isEmpty { + ContentUnavailableView( + "No subscriptions yet", + systemImage: "bell.slash", + description: Text( + "Tap Create Demo to register a Note-created subscription." + ) + ) + } else { + List(rows) { row in + VStack(alignment: .leading, spacing: 2) { + Text(row.id).font(.body.monospaced()) + Text(row.kind) + .font(.caption) + .foregroundStyle(.secondary) + if let recordType = row.recordType { + Text("Record type: \(recordType)") + .font(.caption) + .foregroundStyle(.secondary) + } + } + .deleteSwipeAction { + Task { await deleteSubscription(id: row.id) } + } + } + } + } + .safeAreaInset(edge: .bottom) { lookupBar } + .navigationTitle("Subscriptions") + .toolbar { + ToolbarItem { + Button("Create Demo") { Task { await createDemo() } } + } + ToolbarItem { + Button("Refresh") { Task { await refresh() } } + } + } + .task { await refresh() } + } + + private var lookupBar: some View { + HStack { + TextField( + "Lookup IDs (comma-separated)", + text: $lookupInput + ) + .font(.body.monospaced()) + Button("Lookup") { Task { await runLookup() } } + .disabled(lookupInput.isEmpty) + } + .padding(8) + .background(.thinMaterial) + } + + private func refresh() async { + loading = true + loadError = nil + defer { loading = false } + do { + rows = try await service.loadSubscriptions() + } catch { + loadError = error.localizedDescription + } + } + + private func createDemo() async { + do { + _ = try await service.createDemoSubscription() + await refresh() + } catch { + loadError = error.localizedDescription + } + } + + private func runLookup() async { + let ids = + lookupInput + .split(separator: ",") + .map { String($0).trimmingCharacters(in: .whitespaces) } + .filter { !$0.isEmpty } + guard !ids.isEmpty else { + return + } + loading = true + loadError = nil + defer { loading = false } + do { + rows = try await service.lookupSubscriptions(ids: ids) + } catch { + loadError = error.localizedDescription + } + } + + private func deleteSubscription(id: String) async { + do { + try await service.deleteSubscription(id: id) + await refresh() + } catch { + loadError = error.localizedDescription + } + } + } +#endif diff --git a/Examples/MistDemo/Sources/MistDemoApp/Views/UsersView.swift b/Examples/MistDemo/Sources/MistDemoApp/Views/UsersView.swift new file mode 100644 index 00000000..8aed459e --- /dev/null +++ b/Examples/MistDemo/Sources/MistDemoApp/Views/UsersView.swift @@ -0,0 +1,172 @@ +// +// UsersView.swift +// MistDemo +// +// Created by Leo Dion. +// Copyright © 2026 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +#if canImport(SwiftUI) && canImport(CloudKit) + internal import MistDemoKit + internal import SwiftUI + + /// View driving `users/discover`, `users/lookup/email`, and + /// `users/lookup/id` against native CloudKit. CloudKit only returns + /// identities the caller is permitted to discover, so empty results + /// here usually mean the lookup target hasn't opted in to discovery. + internal struct UsersView: View { + @Environment(CloudKitStore.self) private var service + @State private var emailInput: String = "" + @State private var recordNameInput: String = "" + @State private var discoverInput: String = "" + @State private var results: [UserIdentityRow] = [] + @State private var error: String? + @State private var loading = false + + internal var body: some View { + Form { + emailSection + recordNameSection + discoverSection + if let error { + Section { + Text(error).font(.callout).foregroundStyle(.red) + } + } + if !results.isEmpty { + resultsSection + } + } + .formStyle(.grouped) + .navigationTitle("Users") + } + + private var emailSection: some View { + Section { + TextField("Email address", text: $emailInput) + Button("Lookup by Email") { + Task { await lookupByEmail() } + } + .disabled(emailInput.isEmpty || loading) + } header: { + Text("users/lookup/email") + } + } + + private var recordNameSection: some View { + Section { + TextField("User record name", text: $recordNameInput) + .font(.body.monospaced()) + Button("Lookup by Record Name") { + Task { await lookupByRecordName() } + } + .disabled(recordNameInput.isEmpty || loading) + } header: { + Text("users/lookup/id") + } + } + + private var discoverSection: some View { + Section { + TextField("Comma-separated emails", text: $discoverInput) + Button("Discover") { + Task { await discover() } + } + .disabled(discoverInput.isEmpty || loading) + } header: { + Text("users/discover (POST)") + } footer: { + Text( + "CloudKit JS exposes a per-email primitive only; the batch " + + "POST surface is composed by looping the per-call API." + ) + .font(.caption) + } + } + + private var resultsSection: some View { + Section("Results") { + ForEach(results) { row in + VStack(alignment: .leading, spacing: 2) { + Text(row.displayName ?? "(no display name)") + .font(.headline) + if let recordName = row.recordName { + Text(recordName).font(.caption.monospaced()) + .foregroundStyle(.secondary) + } + if let hint = row.lookupHint { + Text("Looked up via: \(hint)") + .font(.caption).foregroundStyle(.secondary) + } + } + } + } + } + + private func lookupByEmail() async { + await runLookup { + if let row = try await service.lookupUser(byEmail: emailInput) { + return [row] + } + return [] + } + } + + private func lookupByRecordName() async { + await runLookup { + if let row = try await service.lookupUser( + byRecordName: recordNameInput + ) { + return [row] + } + return [] + } + } + + private func discover() async { + let emails = + discoverInput + .split(separator: ",") + .map { String($0).trimmingCharacters(in: .whitespaces) } + .filter { !$0.isEmpty } + await runLookup { + try await service.discoverUsers(byEmails: emails) + } + } + + private func runLookup( + _ operation: @Sendable () async throws -> [UserIdentityRow] + ) async { + loading = true + error = nil + defer { loading = false } + do { + results = try await operation() + } catch { + results = [] + self.error = error.localizedDescription + } + } + } +#endif diff --git a/Examples/MistDemo/Sources/MistDemoApp/Views/View+DeleteSwipeAction.swift b/Examples/MistDemo/Sources/MistDemoApp/Views/View+DeleteSwipeAction.swift new file mode 100644 index 00000000..b40a761a --- /dev/null +++ b/Examples/MistDemo/Sources/MistDemoApp/Views/View+DeleteSwipeAction.swift @@ -0,0 +1,52 @@ +// +// View+DeleteSwipeAction.swift +// MistDemo +// +// Created by Leo Dion. +// Copyright © 2026 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +#if canImport(SwiftUI) + internal import SwiftUI + + extension View { + /// Attaches a trailing destructive "Delete" swipe action. + /// + /// `swipeActions(...)` is unavailable on tvOS, so this is a no-op there; + /// every list row that offers swipe-to-delete routes through this helper + /// so the platform guard lives in exactly one place. + @ViewBuilder + internal func deleteSwipeAction( + perform action: @escaping () -> Void + ) -> some View { + #if os(tvOS) + self + #else + self.swipeActions { + Button("Delete", role: .destructive, action: action) + } + #endif + } + } +#endif diff --git a/Examples/MistDemo/Sources/MistDemoApp/Views/ZoneListView.swift b/Examples/MistDemo/Sources/MistDemoApp/Views/ZoneListView.swift index dd2686e1..5990cbb7 100644 --- a/Examples/MistDemo/Sources/MistDemoApp/Views/ZoneListView.swift +++ b/Examples/MistDemo/Sources/MistDemoApp/Views/ZoneListView.swift @@ -31,12 +31,17 @@ internal import MistDemoKit internal import SwiftUI - /// View listing all CloudKit record zones. + /// View listing all CloudKit record zones, with modify (create/delete) + /// and changes (database-scope sync token) actions. Covers `zones/list`, + /// `zones/lookup`, `zones/modify`, and `zones/changes` in one place. internal struct ZoneListView: View { @Environment(CloudKitStore.self) private var service @State private var zones: [ZoneRow] = [] @State private var loading = false @State private var loadError: String? + @State private var newZoneName: String = "" + @State private var changesSnapshot: DatabaseChangesSnapshot? + @State private var changesError: String? internal var body: some View { Group { @@ -65,14 +70,21 @@ .foregroundStyle(.secondary) } .padding(.vertical, 2) + .deleteSwipeAction { + Task { await deleteZone(named: zone.zoneName) } + } } } } + .safeAreaInset(edge: .bottom) { actionBar } .navigationTitle(service.databaseScope.label.map { "Zones — \($0)" } ?? "Zones") .toolbar { ToolbarItem { Button("Refresh") { Task { await refresh() } } } + ToolbarItem { + Button("Fetch Changes") { Task { await fetchChanges() } } + } } .task { await refresh() } .onChange(of: service.databaseScope) { _, _ in @@ -81,6 +93,39 @@ } } + private var actionBar: some View { + VStack(spacing: 4) { + HStack { + TextField( + "New zone name", text: $newZoneName + ) + .font(.body.monospaced()) + Button("Create") { + Task { await createZone() } + } + .disabled(newZoneName.isEmpty) + } + if let snapshot = changesSnapshot { + HStack { + Text( + "Changed \(snapshot.changedZoneIDs.count), " + + "deleted \(snapshot.deletedZoneIDs.count)" + ) + .font(.caption) + Spacer() + Text(snapshot.moreComing ? "more coming" : "in-sync") + .font(.caption) + .foregroundStyle(.secondary) + } + } + if let changesError { + Text(changesError).font(.caption).foregroundStyle(.red) + } + } + .padding(8) + .background(.thinMaterial) + } + private func refresh() async { loading = true loadError = nil @@ -91,5 +136,35 @@ loadError = error.localizedDescription } } + + private func createZone() async { + do { + _ = try await service.createZone(named: newZoneName) + newZoneName = "" + await refresh() + } catch { + loadError = error.localizedDescription + } + } + + private func deleteZone(named name: String) async { + do { + try await service.deleteZone(named: name) + await refresh() + } catch { + loadError = error.localizedDescription + } + } + + private func fetchChanges() async { + changesError = nil + do { + changesSnapshot = try await service.fetchDatabaseChanges( + since: changesSnapshot?.serverChangeToken + ) + } catch { + changesError = error.localizedDescription + } + } } #endif diff --git a/Examples/MistDemo/Sources/MistDemoKit/Commands/CreateTokenCommand.swift b/Examples/MistDemo/Sources/MistDemoKit/Commands/CreateTokenCommand.swift new file mode 100644 index 00000000..8cb0b636 --- /dev/null +++ b/Examples/MistDemo/Sources/MistDemoKit/Commands/CreateTokenCommand.swift @@ -0,0 +1,69 @@ +// +// CreateTokenCommand.swift +// MistDemo +// +// Created by Leo Dion. +// Copyright © 2026 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +internal import Foundation + +/// Stub command for `tokens/create`. Creates an APNs token CloudKit can use +/// to deliver push notifications to a registered subscription. MistKit +/// Swift wrapper tracked in #52. +public struct CreateTokenCommand: MistDemoCommand { + /// The configuration type. + public typealias Config = CreateTokenConfig + /// The command name. + public static let commandName = "create-token" + /// The command abstract. + public static let abstract = "Create an APNs token for CloudKit subscriptions (pending #52)" + /// The command help text. + public static let helpText = """ + CREATE-TOKEN - Create an APNs token for CloudKit subscriptions + + USAGE: + mistdemo create-token --apns-token [--apns-environment ] + + OPTIONS: + --apns-token APNs device token (hex string) + --apns-environment APNs environment (development, production) + --output-format Output format (json, table, csv, yaml) + + STATUS: + Not yet implemented — pending MistKit support, tracked in #52. + """ + + private let config: CreateTokenConfig + + /// Creates a new instance. + public init(config: CreateTokenConfig) { + self.config = config + } + + /// Executes the command. + public func execute() async throws { + PendingStub.printPending(endpoint: "tokens/create", trackingIssue: 52) + } +} diff --git a/Examples/MistDemo/Sources/MistDemoKit/Commands/ListSubscriptionsCommand.swift b/Examples/MistDemo/Sources/MistDemoKit/Commands/ListSubscriptionsCommand.swift new file mode 100644 index 00000000..e7a48f74 --- /dev/null +++ b/Examples/MistDemo/Sources/MistDemoKit/Commands/ListSubscriptionsCommand.swift @@ -0,0 +1,68 @@ +// +// ListSubscriptionsCommand.swift +// MistDemo +// +// Created by Leo Dion. +// Copyright © 2026 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +internal import Foundation + +/// Stub command for `subscriptions/list`. Lists every CloudKit subscription +/// registered against the selected database. MistKit Swift wrapper tracked +/// in #49. +public struct ListSubscriptionsCommand: MistDemoCommand { + /// The configuration type. + public typealias Config = ListSubscriptionsConfig + /// The command name. + public static let commandName = "list-subscriptions" + /// The command abstract. + public static let abstract = "List CloudKit subscriptions (pending #49)" + /// The command help text. + public static let helpText = """ + LIST-SUBSCRIPTIONS - List CloudKit subscriptions + + USAGE: + mistdemo list-subscriptions [options] + + OPTIONS: + --database Database to target (private, shared, public) + --output-format Output format (json, table, csv, yaml) + + STATUS: + Not yet implemented — pending MistKit support, tracked in #49. + """ + + private let config: ListSubscriptionsConfig + + /// Creates a new instance. + public init(config: ListSubscriptionsConfig) { + self.config = config + } + + /// Executes the command. + public func execute() async throws { + PendingStub.printPending(endpoint: "subscriptions/list", trackingIssue: 49) + } +} diff --git a/Examples/MistDemo/Sources/MistDemoKit/Commands/LookupSubscriptionCommand.swift b/Examples/MistDemo/Sources/MistDemoKit/Commands/LookupSubscriptionCommand.swift new file mode 100644 index 00000000..a78bf14b --- /dev/null +++ b/Examples/MistDemo/Sources/MistDemoKit/Commands/LookupSubscriptionCommand.swift @@ -0,0 +1,68 @@ +// +// LookupSubscriptionCommand.swift +// MistDemo +// +// Created by Leo Dion. +// Copyright © 2026 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +internal import Foundation + +/// Stub command for `subscriptions/lookup`. Looks up one or more +/// CloudKit subscriptions by ID. MistKit Swift wrapper tracked in #50. +public struct LookupSubscriptionCommand: MistDemoCommand { + /// The configuration type. + public typealias Config = LookupSubscriptionConfig + /// The command name. + public static let commandName = "lookup-subscription" + /// The command abstract. + public static let abstract = "Look up a CloudKit subscription by ID (pending #50)" + /// The command help text. + public static let helpText = """ + LOOKUP-SUBSCRIPTION - Look up a CloudKit subscription by ID + + USAGE: + mistdemo lookup-subscription --subscription-ids [options] + + OPTIONS: + --subscription-ids Comma-separated subscription IDs + --database Database to target + --output-format Output format (json, table, csv, yaml) + + STATUS: + Not yet implemented — pending MistKit support, tracked in #50. + """ + + private let config: LookupSubscriptionConfig + + /// Creates a new instance. + public init(config: LookupSubscriptionConfig) { + self.config = config + } + + /// Executes the command. + public func execute() async throws { + PendingStub.printPending(endpoint: "subscriptions/lookup", trackingIssue: 50) + } +} diff --git a/Examples/MistDemo/Sources/MistDemoKit/Commands/QueryCommand+FilterParsing.swift b/Examples/MistDemo/Sources/MistDemoKit/Commands/QueryCommand+FilterParsing.swift index c2e899fc..8c26294c 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Commands/QueryCommand+FilterParsing.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Commands/QueryCommand+FilterParsing.swift @@ -31,6 +31,28 @@ internal import Foundation internal import MistKit extension QueryCommand { + /// Comparison operators keyed by every spelling we accept, each mapped to + /// the matching ``QueryFilter`` case. A lookup table replaces a `switch` + /// that tripped `cyclomatic_complexity`. + private static let comparisonFilterBuilders: + [String: @Sendable (String, FieldValue) -> QueryFilter] = [ + "eq": QueryFilter.equals, + "equals": QueryFilter.equals, + "==": QueryFilter.equals, + "=": QueryFilter.equals, + "ne": QueryFilter.notEquals, + "not_equals": QueryFilter.notEquals, + "!=": QueryFilter.notEquals, + "gt": QueryFilter.greaterThan, + ">": QueryFilter.greaterThan, + "gte": QueryFilter.greaterThanOrEquals, + ">=": QueryFilter.greaterThanOrEquals, + "lt": QueryFilter.lessThan, + "<": QueryFilter.lessThan, + "lte": QueryFilter.lessThanOrEquals, + "<=": QueryFilter.lessThanOrEquals, + ] + /// Parse a single filter expression "field:operator:value" into a QueryFilter internal static func parseFilter(_ filterString: String) throws -> QueryFilter { let components = filterString.split( @@ -69,32 +91,16 @@ extension QueryCommand { } /// Build comparison-based filters (equals, not equals, greater/less than). - // swiftlint:disable:next cyclomatic_complexity internal static func buildComparisonFilter( field: String, operatorString: String, value: String ) -> QueryFilter? { - switch operatorString.lowercased() { - case "eq", "equals", "==", "=": - return .equals(field, inferFieldValue(value)) - case "ne", "not_equals", "!=": - return .notEquals(field, inferFieldValue(value)) - case "gt", ">": - return .greaterThan(field, inferFieldValue(value)) - case "gte", ">=": - return .greaterThanOrEquals( - field, inferFieldValue(value) - ) - case "lt", "<": - return .lessThan(field, inferFieldValue(value)) - case "lte", "<=": - return .lessThanOrEquals( - field, inferFieldValue(value) - ) - default: + guard let makeFilter = comparisonFilterBuilders[operatorString.lowercased()] + else { return nil } + return makeFilter(field, inferFieldValue(value)) } /// Build string and list-based filters. diff --git a/Examples/MistDemo/Sources/MistDemoKit/Commands/RegisterTokenCommand.swift b/Examples/MistDemo/Sources/MistDemoKit/Commands/RegisterTokenCommand.swift new file mode 100644 index 00000000..ed11648e --- /dev/null +++ b/Examples/MistDemo/Sources/MistDemoKit/Commands/RegisterTokenCommand.swift @@ -0,0 +1,70 @@ +// +// RegisterTokenCommand.swift +// MistDemo +// +// Created by Leo Dion. +// Copyright © 2026 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +internal import Foundation + +/// Stub command for `tokens/register`. Wires an APNs token into a CloudKit +/// subscription so push notifications get delivered. MistKit Swift wrapper +/// tracked in #53. +public struct RegisterTokenCommand: MistDemoCommand { + /// The configuration type. + public typealias Config = RegisterTokenConfig + /// The command name. + public static let commandName = "register-token" + /// The command abstract. + public static let abstract = "Register an APNs token with a subscription (pending #53)" + /// The command help text. + public static let helpText = """ + REGISTER-TOKEN - Register an APNs token with a CloudKit subscription + + USAGE: + mistdemo register-token --apns-token --subscription-id + + OPTIONS: + --apns-token APNs device token (hex string) + --subscription-id CloudKit subscription ID + --database Database to target + --output-format Output format (json, table, csv, yaml) + + STATUS: + Not yet implemented — pending MistKit support, tracked in #53. + """ + + private let config: RegisterTokenConfig + + /// Creates a new instance. + public init(config: RegisterTokenConfig) { + self.config = config + } + + /// Executes the command. + public func execute() async throws { + PendingStub.printPending(endpoint: "tokens/register", trackingIssue: 53) + } +} diff --git a/Examples/MistDemo/Sources/MistDemoKit/Commands/RereferenceAssetCommand.swift b/Examples/MistDemo/Sources/MistDemoKit/Commands/RereferenceAssetCommand.swift new file mode 100644 index 00000000..b6fa642f --- /dev/null +++ b/Examples/MistDemo/Sources/MistDemoKit/Commands/RereferenceAssetCommand.swift @@ -0,0 +1,74 @@ +// +// RereferenceAssetCommand.swift +// MistDemo +// +// Created by Leo Dion. +// Copyright © 2026 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +internal import Foundation + +/// Stub command for `assets/rereference`. Reuses an existing CloudKit asset +/// descriptor from one record on another, avoiding a second upload. The +/// MistKit Swift wrapper is tracked in #31. +public struct RereferenceAssetCommand: MistDemoCommand { + /// The configuration type. + public typealias Config = RereferenceAssetConfig + /// The command name. + public static let commandName = "rereference-asset" + /// The command abstract. + public static let abstract = "Re-reference an asset across records (pending #31)" + /// The command help text. + public static let helpText = """ + REREFERENCE-ASSET - Re-reference an existing asset across records + + USAGE: + mistdemo rereference-asset \\ + --source-record --asset-field \\ + --target-record [--target-asset-field ] + + OPTIONS: + --source-record Record name whose asset to reuse + --asset-field Field on the source record holding the asset + --target-record Record name receiving the asset reference + --target-asset-field Field on the target record (defaults to --asset-field) + --database Database to target + --output-format Output format (json, table, csv, yaml) + + STATUS: + Not yet implemented — pending MistKit support, tracked in #31. + """ + + private let config: RereferenceAssetConfig + + /// Creates a new instance. + public init(config: RereferenceAssetConfig) { + self.config = config + } + + /// Executes the command. + public func execute() async throws { + PendingStub.printPending(endpoint: "assets/rereference", trackingIssue: 31) + } +} diff --git a/Examples/MistDemo/Sources/MistDemoKit/Commands/ResolveCommand.swift b/Examples/MistDemo/Sources/MistDemoKit/Commands/ResolveCommand.swift new file mode 100644 index 00000000..60fc4bea --- /dev/null +++ b/Examples/MistDemo/Sources/MistDemoKit/Commands/ResolveCommand.swift @@ -0,0 +1,74 @@ +// +// ResolveCommand.swift +// MistDemo +// +// Created by Leo Dion. +// Copyright © 2026 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +internal import Foundation + +/// Stub command for `records/resolve`. Resolves a share URL or record +/// reference to a CloudKit record. The MistKit Swift wrapper is tracked +/// in #41; until that lands, this command prints the standard pending +/// banner and exits 0 so the `--help` shape is discoverable today. +public struct ResolveCommand: MistDemoCommand { + /// The configuration type. + public typealias Config = ResolveConfig + /// The command name. + public static let commandName = "resolve" + /// The command abstract. + public static let abstract = "Resolve a share URL or record reference (pending #41)" + /// The command help text. + public static let helpText = """ + RESOLVE - Resolve a share URL or record reference + + USAGE: + mistdemo resolve --share-url [options] + mistdemo resolve --record-name [options] + + INPUT (choose one): + --share-url Share URL to resolve + --record-name Record name to resolve + + OPTIONS: + --database Database to target + --output-format Output format (json, table, csv, yaml) + + STATUS: + Not yet implemented — pending MistKit support, tracked in #41. + """ + + private let config: ResolveConfig + + /// Creates a new instance. + public init(config: ResolveConfig) { + self.config = config + } + + /// Executes the command. + public func execute() async throws { + PendingStub.printPending(endpoint: "records/resolve", trackingIssue: 41) + } +} diff --git a/Examples/MistDemo/Sources/MistDemoKit/Configuration/CreateTokenConfig.swift b/Examples/MistDemo/Sources/MistDemoKit/Configuration/CreateTokenConfig.swift new file mode 100644 index 00000000..f7edc264 --- /dev/null +++ b/Examples/MistDemo/Sources/MistDemoKit/Configuration/CreateTokenConfig.swift @@ -0,0 +1,91 @@ +// +// CreateTokenConfig.swift +// MistDemo +// +// Created by Leo Dion. +// Copyright © 2026 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +public import ConfigKeyKit +internal import Foundation + +/// Configuration for the `create-token` command. +public struct CreateTokenConfig: Sendable, ConfigurationParseable { + /// The configuration reader type. + public typealias ConfigReader = MistDemoConfiguration + /// The base configuration type. + public typealias BaseConfig = MistDemoConfig + + /// The base MistDemo configuration. + public let base: MistDemoConfig + /// APNs device token (hex string). + public let apnsToken: String? + /// APNs environment (development, production). + public let apnsEnvironment: String? + /// The output format. + public let output: OutputFormat + + /// Creates a new instance. + public init( + base: MistDemoConfig, + apnsToken: String? = nil, + apnsEnvironment: String? = nil, + output: OutputFormat = .json + ) { + self.base = base + self.apnsToken = apnsToken + self.apnsEnvironment = apnsEnvironment + self.output = output + } + + /// Parse configuration from command line arguments. + public init( + configuration: MistDemoConfiguration, + base: MistDemoConfig? + ) async throws { + let baseConfig: MistDemoConfig + if let base { + baseConfig = base + } else { + baseConfig = try await MistDemoConfig( + configuration: configuration, + base: nil + ) + } + + let outputString = + configuration.string( + forKey: MistDemoConstants.ConfigKeys.outputFormat, + default: "json" + ) ?? "json" + let output = OutputFormat(rawValue: outputString) ?? .json + + self.init( + base: baseConfig, + apnsToken: configuration.string(forKey: "apns-token"), + apnsEnvironment: configuration.string(forKey: "apns-environment"), + output: output + ) + } +} diff --git a/Examples/MistDemo/Sources/MistDemoKit/Configuration/ListSubscriptionsConfig.swift b/Examples/MistDemo/Sources/MistDemoKit/Configuration/ListSubscriptionsConfig.swift new file mode 100644 index 00000000..f5bc571c --- /dev/null +++ b/Examples/MistDemo/Sources/MistDemoKit/Configuration/ListSubscriptionsConfig.swift @@ -0,0 +1,78 @@ +// +// ListSubscriptionsConfig.swift +// MistDemo +// +// Created by Leo Dion. +// Copyright © 2026 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +public import ConfigKeyKit +internal import Foundation + +/// Configuration for the `list-subscriptions` command. +public struct ListSubscriptionsConfig: Sendable, ConfigurationParseable { + /// The configuration reader type. + public typealias ConfigReader = MistDemoConfiguration + /// The base configuration type. + public typealias BaseConfig = MistDemoConfig + + /// The base MistDemo configuration. + public let base: MistDemoConfig + /// The output format. + public let output: OutputFormat + + /// Creates a new instance. + public init( + base: MistDemoConfig, + output: OutputFormat = .table + ) { + self.base = base + self.output = output + } + + /// Parse configuration from command line arguments. + public init( + configuration: MistDemoConfiguration, + base: MistDemoConfig? + ) async throws { + let baseConfig: MistDemoConfig + if let base { + baseConfig = base + } else { + baseConfig = try await MistDemoConfig( + configuration: configuration, + base: nil + ) + } + + let outputString = + configuration.string( + forKey: MistDemoConstants.ConfigKeys.outputFormat, + default: "table" + ) ?? "table" + let output = OutputFormat(rawValue: outputString) ?? .table + + self.init(base: baseConfig, output: output) + } +} diff --git a/Examples/MistDemo/Sources/MistDemoKit/Configuration/LookupSubscriptionConfig.swift b/Examples/MistDemo/Sources/MistDemoKit/Configuration/LookupSubscriptionConfig.swift new file mode 100644 index 00000000..ac5e23b2 --- /dev/null +++ b/Examples/MistDemo/Sources/MistDemoKit/Configuration/LookupSubscriptionConfig.swift @@ -0,0 +1,93 @@ +// +// LookupSubscriptionConfig.swift +// MistDemo +// +// Created by Leo Dion. +// Copyright © 2026 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +public import ConfigKeyKit +internal import Foundation + +/// Configuration for the `lookup-subscription` command. +public struct LookupSubscriptionConfig: Sendable, ConfigurationParseable { + /// The configuration reader type. + public typealias ConfigReader = MistDemoConfiguration + /// The base configuration type. + public typealias BaseConfig = MistDemoConfig + + /// The base MistDemo configuration. + public let base: MistDemoConfig + /// The subscription IDs to look up. + public let subscriptionIDs: [String] + /// The output format. + public let output: OutputFormat + + /// Creates a new instance. + public init( + base: MistDemoConfig, + subscriptionIDs: [String] = [], + output: OutputFormat = .json + ) { + self.base = base + self.subscriptionIDs = subscriptionIDs + self.output = output + } + + /// Parse configuration from command line arguments. + public init( + configuration: MistDemoConfiguration, + base: MistDemoConfig? + ) async throws { + let baseConfig: MistDemoConfig + if let base { + baseConfig = base + } else { + baseConfig = try await MistDemoConfig( + configuration: configuration, + base: nil + ) + } + + let idsString = configuration.string(forKey: "subscription-ids") ?? "" + let subscriptionIDs = + idsString + .split(separator: ",") + .map { String($0).trimmingCharacters(in: .whitespaces) } + .filter { !$0.isEmpty } + + let outputString = + configuration.string( + forKey: MistDemoConstants.ConfigKeys.outputFormat, + default: "json" + ) ?? "json" + let output = OutputFormat(rawValue: outputString) ?? .json + + self.init( + base: baseConfig, + subscriptionIDs: subscriptionIDs, + output: output + ) + } +} diff --git a/Examples/MistDemo/Sources/MistDemoKit/Configuration/RegisterTokenConfig.swift b/Examples/MistDemo/Sources/MistDemoKit/Configuration/RegisterTokenConfig.swift new file mode 100644 index 00000000..0a2d15e7 --- /dev/null +++ b/Examples/MistDemo/Sources/MistDemoKit/Configuration/RegisterTokenConfig.swift @@ -0,0 +1,91 @@ +// +// RegisterTokenConfig.swift +// MistDemo +// +// Created by Leo Dion. +// Copyright © 2026 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +public import ConfigKeyKit +internal import Foundation + +/// Configuration for the `register-token` command. +public struct RegisterTokenConfig: Sendable, ConfigurationParseable { + /// The configuration reader type. + public typealias ConfigReader = MistDemoConfiguration + /// The base configuration type. + public typealias BaseConfig = MistDemoConfig + + /// The base MistDemo configuration. + public let base: MistDemoConfig + /// APNs device token (hex string). + public let apnsToken: String? + /// CloudKit subscription ID to wire the token into. + public let subscriptionID: String? + /// The output format. + public let output: OutputFormat + + /// Creates a new instance. + public init( + base: MistDemoConfig, + apnsToken: String? = nil, + subscriptionID: String? = nil, + output: OutputFormat = .json + ) { + self.base = base + self.apnsToken = apnsToken + self.subscriptionID = subscriptionID + self.output = output + } + + /// Parse configuration from command line arguments. + public init( + configuration: MistDemoConfiguration, + base: MistDemoConfig? + ) async throws { + let baseConfig: MistDemoConfig + if let base { + baseConfig = base + } else { + baseConfig = try await MistDemoConfig( + configuration: configuration, + base: nil + ) + } + + let outputString = + configuration.string( + forKey: MistDemoConstants.ConfigKeys.outputFormat, + default: "json" + ) ?? "json" + let output = OutputFormat(rawValue: outputString) ?? .json + + self.init( + base: baseConfig, + apnsToken: configuration.string(forKey: "apns-token"), + subscriptionID: configuration.string(forKey: "subscription-id"), + output: output + ) + } +} diff --git a/Examples/MistDemo/Sources/MistDemoKit/Configuration/RereferenceAssetConfig.swift b/Examples/MistDemo/Sources/MistDemoKit/Configuration/RereferenceAssetConfig.swift new file mode 100644 index 00000000..6a1d8e95 --- /dev/null +++ b/Examples/MistDemo/Sources/MistDemoKit/Configuration/RereferenceAssetConfig.swift @@ -0,0 +1,101 @@ +// +// RereferenceAssetConfig.swift +// MistDemo +// +// Created by Leo Dion. +// Copyright © 2026 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +public import ConfigKeyKit +internal import Foundation + +/// Configuration for the `rereference-asset` command. +public struct RereferenceAssetConfig: Sendable, ConfigurationParseable { + /// The configuration reader type. + public typealias ConfigReader = MistDemoConfiguration + /// The base configuration type. + public typealias BaseConfig = MistDemoConfig + + /// The base MistDemo configuration. + public let base: MistDemoConfig + /// Source record name whose asset is being reused. + public let sourceRecord: String? + /// Field on the source record holding the asset. + public let assetField: String? + /// Target record name receiving the asset reference. + public let targetRecord: String? + /// Field on the target record (defaults to `assetField`). + public let targetAssetField: String? + /// The output format. + public let output: OutputFormat + + /// Creates a new instance. + public init( + base: MistDemoConfig, + sourceRecord: String? = nil, + assetField: String? = nil, + targetRecord: String? = nil, + targetAssetField: String? = nil, + output: OutputFormat = .json + ) { + self.base = base + self.sourceRecord = sourceRecord + self.assetField = assetField + self.targetRecord = targetRecord + self.targetAssetField = targetAssetField + self.output = output + } + + /// Parse configuration from command line arguments. + public init( + configuration: MistDemoConfiguration, + base: MistDemoConfig? + ) async throws { + let baseConfig: MistDemoConfig + if let base { + baseConfig = base + } else { + baseConfig = try await MistDemoConfig( + configuration: configuration, + base: nil + ) + } + + let outputString = + configuration.string( + forKey: MistDemoConstants.ConfigKeys.outputFormat, + default: "json" + ) ?? "json" + let output = OutputFormat(rawValue: outputString) ?? .json + + self.init( + base: baseConfig, + sourceRecord: configuration.string(forKey: "source-record"), + assetField: configuration.string(forKey: "asset-field"), + targetRecord: configuration.string(forKey: "target-record"), + targetAssetField: configuration.string(forKey: "target-asset-field"), + output: output + ) + } +} diff --git a/Examples/MistDemo/Sources/MistDemoKit/Configuration/ResolveConfig.swift b/Examples/MistDemo/Sources/MistDemoKit/Configuration/ResolveConfig.swift new file mode 100644 index 00000000..3bacfe31 --- /dev/null +++ b/Examples/MistDemo/Sources/MistDemoKit/Configuration/ResolveConfig.swift @@ -0,0 +1,94 @@ +// +// ResolveConfig.swift +// MistDemo +// +// Created by Leo Dion. +// Copyright © 2026 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +public import ConfigKeyKit +internal import Foundation + +/// Configuration for the `resolve` command. Parses the future argument shape +/// even though `ResolveCommand.execute()` is currently a `PendingStub` +/// — the `--help` text and argument names stabilize here so callers can +/// integrate against the real surface when #41 lands. +public struct ResolveConfig: Sendable, ConfigurationParseable { + /// The configuration reader type. + public typealias ConfigReader = MistDemoConfiguration + /// The base configuration type. + public typealias BaseConfig = MistDemoConfig + + /// The base MistDemo configuration. + public let base: MistDemoConfig + /// Optional share URL to resolve. + public let shareURL: String? + /// Optional record name to resolve. + public let recordName: String? + /// The output format. + public let output: OutputFormat + + /// Creates a new instance. + public init( + base: MistDemoConfig, + shareURL: String? = nil, + recordName: String? = nil, + output: OutputFormat = .json + ) { + self.base = base + self.shareURL = shareURL + self.recordName = recordName + self.output = output + } + + /// Parse configuration from command line arguments. + public init( + configuration: MistDemoConfiguration, + base: MistDemoConfig? + ) async throws { + let baseConfig: MistDemoConfig + if let base { + baseConfig = base + } else { + baseConfig = try await MistDemoConfig( + configuration: configuration, + base: nil + ) + } + + let outputString = + configuration.string( + forKey: MistDemoConstants.ConfigKeys.outputFormat, + default: "json" + ) ?? "json" + let output = OutputFormat(rawValue: outputString) ?? .json + + self.init( + base: baseConfig, + shareURL: configuration.string(forKey: "share-url"), + recordName: configuration.string(forKey: "record-name"), + output: output + ) + } +} diff --git a/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/CreateTokenPhase.swift b/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/CreateTokenPhase.swift new file mode 100644 index 00000000..99de8863 --- /dev/null +++ b/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/CreateTokenPhase.swift @@ -0,0 +1,48 @@ +// +// CreateTokenPhase.swift +// MistDemo +// +// Created by Leo Dion. +// Copyright © 2026 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +internal import Foundation + +/// Stub phase for `tokens/create`. Not wired into the public/private +/// pipelines yet; `#52` flips this into a real run when the MistKit Swift +/// wrapper lands. +internal struct CreateTokenPhase: IntegrationPhase { + internal typealias Input = NoState + internal typealias Output = NoState + + internal static let title = "Create token (pending #52)" + internal static let emoji = "🎟️" + internal static let apiName = "createToken" + + internal func run(input: NoState, context: PhaseContext) async throws -> NoState { + print("\n\(Self.emoji) \(Self.title)") + PendingStub.printPending(endpoint: "tokens/create", trackingIssue: 52) + return NoState() + } +} diff --git a/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/ListSubscriptionsPhase.swift b/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/ListSubscriptionsPhase.swift new file mode 100644 index 00000000..1d77f267 --- /dev/null +++ b/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/ListSubscriptionsPhase.swift @@ -0,0 +1,48 @@ +// +// ListSubscriptionsPhase.swift +// MistDemo +// +// Created by Leo Dion. +// Copyright © 2026 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +internal import Foundation + +/// Stub phase for `subscriptions/list`. Not wired into the public/private +/// pipelines yet; `#49` flips this into a real run when the MistKit Swift +/// wrapper lands. +internal struct ListSubscriptionsPhase: IntegrationPhase { + internal typealias Input = NoState + internal typealias Output = NoState + + internal static let title = "List subscriptions (pending #49)" + internal static let emoji = "🔔" + internal static let apiName = "listSubscriptions" + + internal func run(input: NoState, context: PhaseContext) async throws -> NoState { + print("\n\(Self.emoji) \(Self.title)") + PendingStub.printPending(endpoint: "subscriptions/list", trackingIssue: 49) + return NoState() + } +} diff --git a/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/LookupSubscriptionPhase.swift b/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/LookupSubscriptionPhase.swift new file mode 100644 index 00000000..f9cab48f --- /dev/null +++ b/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/LookupSubscriptionPhase.swift @@ -0,0 +1,48 @@ +// +// LookupSubscriptionPhase.swift +// MistDemo +// +// Created by Leo Dion. +// Copyright © 2026 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +internal import Foundation + +/// Stub phase for `subscriptions/lookup`. Not wired into the public/private +/// pipelines yet; `#50` flips this into a real run when the MistKit Swift +/// wrapper lands. +internal struct LookupSubscriptionPhase: IntegrationPhase { + internal typealias Input = NoState + internal typealias Output = NoState + + internal static let title = "Lookup subscription (pending #50)" + internal static let emoji = "🔍" + internal static let apiName = "lookupSubscription" + + internal func run(input: NoState, context: PhaseContext) async throws -> NoState { + print("\n\(Self.emoji) \(Self.title)") + PendingStub.printPending(endpoint: "subscriptions/lookup", trackingIssue: 50) + return NoState() + } +} diff --git a/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/RegisterTokenPhase.swift b/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/RegisterTokenPhase.swift new file mode 100644 index 00000000..d7535da9 --- /dev/null +++ b/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/RegisterTokenPhase.swift @@ -0,0 +1,48 @@ +// +// RegisterTokenPhase.swift +// MistDemo +// +// Created by Leo Dion. +// Copyright © 2026 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +internal import Foundation + +/// Stub phase for `tokens/register`. Not wired into the public/private +/// pipelines yet; `#53` flips this into a real run when the MistKit Swift +/// wrapper lands. +internal struct RegisterTokenPhase: IntegrationPhase { + internal typealias Input = NoState + internal typealias Output = NoState + + internal static let title = "Register token (pending #53)" + internal static let emoji = "📨" + internal static let apiName = "registerToken" + + internal func run(input: NoState, context: PhaseContext) async throws -> NoState { + print("\n\(Self.emoji) \(Self.title)") + PendingStub.printPending(endpoint: "tokens/register", trackingIssue: 53) + return NoState() + } +} diff --git a/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/RereferenceAssetPhase.swift b/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/RereferenceAssetPhase.swift new file mode 100644 index 00000000..4fe8cda7 --- /dev/null +++ b/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/RereferenceAssetPhase.swift @@ -0,0 +1,48 @@ +// +// RereferenceAssetPhase.swift +// MistDemo +// +// Created by Leo Dion. +// Copyright © 2026 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +internal import Foundation + +/// Stub phase for `assets/rereference`. Not wired into the public/private +/// pipelines yet; `#31` flips this into a real run when the MistKit Swift +/// wrapper lands. +internal struct RereferenceAssetPhase: IntegrationPhase { + internal typealias Input = NoState + internal typealias Output = NoState + + internal static let title = "Rereference asset (pending #31)" + internal static let emoji = "📎" + internal static let apiName = "rereferenceAsset" + + internal func run(input: NoState, context: PhaseContext) async throws -> NoState { + print("\n\(Self.emoji) \(Self.title)") + PendingStub.printPending(endpoint: "assets/rereference", trackingIssue: 31) + return NoState() + } +} diff --git a/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/ResolveRecordsPhase.swift b/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/ResolveRecordsPhase.swift new file mode 100644 index 00000000..7f66dc21 --- /dev/null +++ b/Examples/MistDemo/Sources/MistDemoKit/Integration/Phases/ResolveRecordsPhase.swift @@ -0,0 +1,48 @@ +// +// ResolveRecordsPhase.swift +// MistDemo +// +// Created by Leo Dion. +// Copyright © 2026 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +internal import Foundation + +/// Stub phase for `records/resolve`. The pipeline does not wire this phase +/// into `PublicDatabaseTest` / `PrivateDatabaseTest` yet — it stays available +/// for `#41` to flip into a real run when the MistKit Swift wrapper lands. +internal struct ResolveRecordsPhase: IntegrationPhase { + internal typealias Input = NoState + internal typealias Output = NoState + + internal static let title = "Resolve records (pending #41)" + internal static let emoji = "🔗" + internal static let apiName = "resolveRecords" + + internal func run(input: NoState, context: PhaseContext) async throws -> NoState { + print("\n\(Self.emoji) \(Self.title)") + PendingStub.printPending(endpoint: "records/resolve", trackingIssue: 41) + return NoState() + } +} diff --git a/Examples/MistDemo/Sources/MistDemoKit/MistDemoRunner.swift b/Examples/MistDemo/Sources/MistDemoKit/MistDemoRunner.swift index 4ce77776..e38b85e1 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/MistDemoRunner.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/MistDemoRunner.swift @@ -65,6 +65,15 @@ public enum MistDemoRunner { await registry.register(TestPrivateCommand.self) await registry.register(DemoErrorsCommand.self) + // Pending MistKit wrappers — print "pending #N" and exit 0. Each + // command flips to a real implementation when its tracking issue lands. + await registry.register(ResolveCommand.self) + await registry.register(RereferenceAssetCommand.self) + await registry.register(ListSubscriptionsCommand.self) + await registry.register(LookupSubscriptionCommand.self) + await registry.register(CreateTokenCommand.self) + await registry.register(RegisterTokenCommand.self) + // Parse command line arguments let parser = CommandLineParser() diff --git a/Examples/MistDemo/Sources/MistDemoKit/PushTokenStatus.swift b/Examples/MistDemo/Sources/MistDemoKit/PushTokenStatus.swift new file mode 100644 index 00000000..d8da4e0e --- /dev/null +++ b/Examples/MistDemo/Sources/MistDemoKit/PushTokenStatus.swift @@ -0,0 +1,38 @@ +// +// PushTokenStatus.swift +// MistDemo +// +// Created by Leo Dion. +// Copyright © 2026 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +/// The push-token registration state surfaced to the UI. APNs requires a +/// signed app + push entitlement; on simulators or unentitled builds the +/// OS reports an error which the UI renders inline. +public enum PushTokenStatus: Sendable { + case idle + case requesting + case registered(hexToken: String) + case failed(message: String) +} diff --git a/Examples/MistDemo/Sources/MistDemoKit/Resources/index.html b/Examples/MistDemo/Sources/MistDemoKit/Resources/index.html index 2bf13fb0..55f582a5 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Resources/index.html +++ b/Examples/MistDemo/Sources/MistDemoKit/Resources/index.html @@ -4,220 +4,7 @@ MistKit Web Demo - + @@ -225,9 +12,9 @@

MistKit Web Demo

- Authenticate with your Apple ID, then exercise the same CloudKit - operations through MistKit (server) or CloudKit JS (browser) and - compare the wire-level behavior. + Authenticate with your Apple ID, then exercise the v1.0.0-beta.2 CloudKit + surface through MistKit (server) or CloudKit JS (browser) and compare the + wire-level behavior side-by-side.

Backend

@@ -250,8 +37,7 @@

Database

Private uses the captured Apple ID web-auth token; Public uses server-to-server signing on the MistKit side and the API token on - the CloudKit JS side. Browsers can't perform S2S signing, so - "MistKit + Public" is unique to the server path. + the CloudKit JS side.

Auth

@@ -262,8 +48,11 @@

Auth

+
-

Notes MistKit Private

+

Notes MistKit Private + records/query · records/modify +

@@ -324,732 +113,298 @@

- - - + + + + + + + + + + diff --git a/Examples/MistDemo/Sources/MistDemoKit/Resources/js/app.js b/Examples/MistDemo/Sources/MistDemoKit/Resources/js/app.js new file mode 100644 index 00000000..31377a6d --- /dev/null +++ b/Examples/MistDemo/Sources/MistDemoKit/Resources/js/app.js @@ -0,0 +1,571 @@ +// Shared globals + helpers for the MistDemo web page. +// +// Each operation module (records.js, zones.js, etc.) reads `currentMode` / +// `currentDatabase` set here, and calls these helpers to render output to +// per-panel `
` and `
` elements. This module also
+// owns the existing Notes CRUD panel — it's the originating shape every
+// new panel mirrors.
+
+let container = null;
+let webAuthToken = null;
+let authenticationInProgress = false;
+let currentMode = 'mistkit';            // 'mistkit' | 'cloudkitjs'
+let currentDatabase = 'private';        // 'private' | 'public'
+let publicDatabaseAvailable = false;
+let notes = [];
+let selectedRecordName = null;
+let authComplete = false;
+let queryInFlight = false;
+let currentUserRecordName = null;
+let currentSort = { field: '___createTime', ascending: false };
+const REFRESH_DELAY_MS = 1200;
+
+const authStatusDiv = document.getElementById('auth-status');
+const signinButton = document.getElementById('signin-button');
+const signoutButton = document.getElementById('signout-button');
+const notesCard = document.getElementById('notes-card');
+const modeBadge = document.getElementById('mode-badge');
+const dbBadge = document.getElementById('db-badge');
+const dbPrivateBtn = document.getElementById('db-private');
+const dbPublicBtn = document.getElementById('db-public');
+const dbHint = document.getElementById('db-hint');
+const tbody = document.getElementById('notes-tbody');
+const tableStatusEl = document.getElementById('table-status');
+const formStatusEl = document.getElementById('form-status');
+const formHeading = document.getElementById('form-heading');
+const formRecordName = document.getElementById('form-record-name');
+const titleInput = document.getElementById('form-title');
+const indexInput = document.getElementById('form-index');
+const saveBtn = document.getElementById('save-btn');
+const clearBtn = document.getElementById('clear-btn');
+const deleteBtn = document.getElementById('delete-btn');
+const refreshBtn = document.getElementById('refresh-btn');
+const recordTypeInput = document.getElementById('record-type');
+const queryLimitInput = document.getElementById('query-limit');
+const rawResponseEl = document.getElementById('raw-response');
+
+// ---- shared helpers ----
+
+function setStatus(el, message, kind) {
+    if (!el) return;
+    el.className = `status ${kind || ''}`;
+    el.textContent = message;
+    if (kind) el.style.display = 'block';
+}
+
+function clearStatus(el) {
+    if (!el) return;
+    el.className = 'status';
+    el.textContent = '';
+    el.style.display = 'none';
+}
+
+// JSON.stringify replacer that renders Dates as ISO strings and drops
+// circular references. Some CloudKit JS results (e.g. the value resolved
+// by `registerForNotifications`) hold cyclic structures that would
+// otherwise throw "JSON.stringify cannot serialize cyclic structures".
+function safeReplacer() {
+    const seen = new WeakSet();
+    return (_key, value) => {
+        if (value instanceof Date) return value.toISOString();
+        if (value && typeof value === 'object') {
+            if (seen.has(value)) return '[Circular]';
+            seen.add(value);
+        }
+        return value;
+    };
+}
+
+function showRaw(value) {
+    rawResponseEl.textContent = value == null ? '(none)' : JSON.stringify(value, safeReplacer(), 2);
+}
+
+// Render an arbitrary payload to a specific 
 element (used by all
+// new operation panels). Mirrors `showRaw` but takes the target element
+// explicitly.
+function renderRaw(el, value) {
+    if (!el) return;
+    el.textContent = value == null
+        ? '(none)'
+        : JSON.stringify(value, safeReplacer(), 2);
+}
+
+// Render a simple read-only table of `items` into `tbody`. `getters` is an
+// array of `item => cellValue` functions, one per column — its length must
+// match the table's column count. Used by the Zones and Subscriptions list
+// panels to present results as a table instead of raw JSON.
+function renderListTable(tbody, getters, items, emptyMessage) {
+    if (!tbody) return;
+    tbody.innerHTML = '';
+    if (!items || items.length === 0) {
+        const tr = document.createElement('tr');
+        const td = document.createElement('td');
+        td.colSpan = getters.length;
+        td.className = 'empty-state';
+        td.textContent = emptyMessage;
+        tr.appendChild(td);
+        tbody.appendChild(tr);
+        return;
+    }
+    for (const item of items) {
+        const tr = document.createElement('tr');
+        for (const get of getters) {
+            const td = document.createElement('td');
+            const value = get(item);
+            td.textContent = (value == null || value === '') ? '—' : String(value);
+            tr.appendChild(td);
+        }
+        tbody.appendChild(tr);
+    }
+}
+
+// Run an operation and pipe its progress through the panel's status div
+// + raw `
`. Common shape across every new panel:
+//   - "loading…" while in-flight
+//   - success body rendered as JSON in the pre
+//   - errors rendered as red banner + payload (or message) in the pre
+async function runPanelOperation({ statusEl, rawEl, label, fn }) {
+    setStatus(statusEl, `${label}…`, 'loading');
+    try {
+        const result = await fn();
+        renderRaw(rawEl, result);
+        setStatus(statusEl, `${label} succeeded.`, 'success');
+        return result;
+    } catch (error) {
+        const payload = error && error.payload ? error.payload : { message: error.message };
+        renderRaw(rawEl, payload);
+        const msg = (error && error.message) || 'Unknown error';
+        setStatus(statusEl, `${label} failed: ${msg}`, 'error');
+        return null;
+    }
+}
+
+async function postJSON(path, body) {
+    const init = { headers: { 'Content-Type': 'application/json' } };
+    if (body !== undefined) {
+        init.method = 'POST';
+        init.body = JSON.stringify(body);
+    }
+    const response = await fetch(path, init);
+    const text = await response.text();
+    let payload;
+    try { payload = text ? JSON.parse(text) : null; } catch { payload = { message: text }; }
+    if (!response.ok) {
+        const msg = (payload && (payload.message || payload.error)) || `HTTP ${response.status}`;
+        const err = new Error(msg);
+        err.payload = payload;
+        err.status = response.status;
+        throw err;
+    }
+    return payload;
+}
+
+async function fetchJSON(path) {
+    const response = await fetch(path);
+    const text = await response.text();
+    let payload;
+    try { payload = text ? JSON.parse(text) : null; } catch { payload = { message: text }; }
+    if (!response.ok) {
+        const msg = (payload && (payload.message || payload.error)) || `HTTP ${response.status}`;
+        const err = new Error(msg);
+        err.payload = payload;
+        err.status = response.status;
+        throw err;
+    }
+    return payload;
+}
+
+function ckJsDatabase() {
+    return currentDatabase === 'public'
+        ? container.publicCloudDatabase
+        : container.privateCloudDatabase;
+}
+
+function ckJsContainer() {
+    return container;
+}
+
+function csv(value) {
+    return (value || '')
+        .split(',')
+        .map(s => s.trim())
+        .filter(s => s.length > 0);
+}
+
+// ---- form state for Notes CRUD ----
+
+function selectedNote() {
+    return notes.find(n => n.recordName === selectedRecordName) || null;
+}
+
+function refreshFormState() {
+    const note = selectedNote();
+    if (note) {
+        formHeading.textContent = 'Edit note';
+        formRecordName.textContent = `· ${note.recordName}`;
+        saveBtn.textContent = 'Save';
+        deleteBtn.disabled = false;
+    } else {
+        formHeading.textContent = 'New note';
+        formRecordName.textContent = '';
+        saveBtn.textContent = 'Create';
+        deleteBtn.disabled = true;
+    }
+}
+
+function clearForm() {
+    selectedRecordName = null;
+    titleInput.value = '';
+    indexInput.value = '';
+    clearStatus(formStatusEl);
+    refreshFormState();
+    renderRows();
+}
+
+function loadNoteIntoForm(note) {
+    selectedRecordName = note.recordName;
+    titleInput.value = note.title ?? '';
+    indexInput.value = note.index != null ? String(note.index) : '';
+    clearStatus(formStatusEl);
+    refreshFormState();
+    renderRows();
+}
+
+// ---- render Notes table ----
+
+function renderRows() {
+    tbody.innerHTML = '';
+    if (notes.length === 0) {
+        const tr = document.createElement('tr');
+        const td = document.createElement('td');
+        td.colSpan = 5;
+        td.className = 'empty-state';
+        td.textContent = 'No notes — Refresh or Create one.';
+        tr.appendChild(td);
+        tbody.appendChild(tr);
+        return;
+    }
+    for (const note of notes) {
+        const tr = document.createElement('tr');
+        tr.title = note.recordName;
+        if (note.recordName === selectedRecordName) tr.classList.add('selected');
+        tr.addEventListener('click', (e) => {
+            if (e.target.closest('button')) return;
+            loadNoteIntoForm(note);
+        });
+
+        const titleTd = document.createElement('td');
+        titleTd.textContent = note.title ?? '';
+        if (note.createdBy && note.createdBy === currentUserRecordName) {
+            const youBadge = document.createElement('span');
+            youBadge.className = 'badge badge-you';
+            youBadge.textContent = 'You';
+            youBadge.title = `Created by ${note.createdBy}`;
+            titleTd.appendChild(youBadge);
+        }
+        tr.appendChild(titleTd);
+
+        const indexTd = document.createElement('td');
+        indexTd.textContent = note.index != null ? String(note.index) : '';
+        tr.appendChild(indexTd);
+
+        const createdTd = document.createElement('td');
+        createdTd.className = 'timestamp';
+        createdTd.textContent = formatTimestamp(note.created);
+        if (note.created) createdTd.title = note.created.toISOString();
+        tr.appendChild(createdTd);
+
+        const modifiedTd = document.createElement('td');
+        modifiedTd.className = 'timestamp';
+        modifiedTd.textContent = formatTimestamp(note.modified);
+        if (note.modified) modifiedTd.title = note.modified.toISOString();
+        tr.appendChild(modifiedTd);
+
+        const actionsTd = document.createElement('td');
+        actionsTd.className = 'actions';
+        const delBtn = document.createElement('button');
+        delBtn.className = 'danger';
+        delBtn.type = 'button';
+        delBtn.textContent = 'Delete';
+        delBtn.addEventListener('click', () => deleteNote(note));
+        actionsTd.appendChild(delBtn);
+        tr.appendChild(actionsTd);
+
+        tbody.appendChild(tr);
+    }
+}
+
+function refreshSortIndicators() {
+    document.querySelectorAll('th.sortable').forEach(th => {
+        const field = th.dataset.sortField;
+        const isActive = currentSort && currentSort.field === field;
+        th.classList.toggle('active', isActive);
+        const indicator = th.querySelector('.sort-indicator');
+        if (isActive) {
+            indicator.textContent = currentSort.ascending ? '↑' : '↓';
+        } else {
+            indicator.textContent = '';
+        }
+    });
+}
+
+document.querySelectorAll('th.sortable').forEach(th => {
+    th.addEventListener('click', () => {
+        const field = th.dataset.sortField;
+        if (currentSort && currentSort.field === field) {
+            currentSort = currentSort.ascending
+                ? { field, ascending: false }
+                : null;
+        } else {
+            currentSort = { field, ascending: true };
+        }
+        refreshSortIndicators();
+        if (authComplete) queryNotes();
+    });
+});
+
+// ---- payload normalization ----
+
+function normalizeRecords(payload) {
+    const list = (payload && payload.records) || [];
+    return list.map(record => {
+        const fields = record.fields || {};
+        const titleField = fields.title;
+        const indexField = fields.index;
+        return {
+            recordName: record.recordName,
+            recordType: record.recordType,
+            recordChangeTag: record.recordChangeTag,
+            title: titleField && (titleField.value ?? titleField),
+            index: indexField && Number(indexField.value ?? indexField),
+            created: toDate(record.created),
+            modified: toDate(record.modified),
+            createdBy: extractUserRecordName(record.created),
+            raw: record,
+        };
+    });
+}
+
+function extractUserRecordName(value) {
+    if (value == null || typeof value !== 'object' || value instanceof Date) {
+        return null;
+    }
+    return value.userRecordName ?? null;
+}
+
+function toDate(value) {
+    if (value == null) return null;
+    if (value instanceof Date) return value;
+    if (typeof value === 'number') return new Date(value);
+    if (typeof value === 'object') {
+        const inner = value.timestamp;
+        if (inner == null) return null;
+        if (inner instanceof Date) return inner;
+        if (typeof inner === 'number') return new Date(inner);
+        if (typeof inner === 'string') {
+            const parsed = Date.parse(inner);
+            return isNaN(parsed) ? null : new Date(parsed);
+        }
+    }
+    return null;
+}
+
+function formatTimestamp(date) {
+    if (!date) return '—';
+    return date.toLocaleString(undefined, {
+        dateStyle: 'short', timeStyle: 'short',
+    });
+}
+
+function buildFields() {
+    const out = {};
+    const title = titleInput.value.trim();
+    if (title.length > 0) out.title = title;
+    const indexRaw = indexInput.value.trim();
+    if (indexRaw.length > 0) {
+        const parsed = Number(indexRaw);
+        if (!isFinite(parsed)) {
+            throw new Error('Index must be a number.');
+        }
+        out.index = parsed;
+    }
+    return out;
+}
+
+function ckJsFields(fields) {
+    const wrapped = {};
+    for (const [k, v] of Object.entries(fields)) {
+        wrapped[k] = { value: v };
+    }
+    return wrapped;
+}
+
+// ---- Notes CRUD operations ----
+
+async function queryNotes() {
+    if (queryInFlight) return;
+    const recordType = recordTypeInput.value.trim();
+    const limit = parseInt(queryLimitInput.value, 10);
+    queryInFlight = true;
+    setQueryControlsDisabled(true);
+    const dbLabel = currentDatabase === 'public' ? 'public' : 'private';
+    const modeLabel = currentMode === 'mistkit' ? 'MistKit' : 'CloudKit JS';
+    setStatus(tableStatusEl, `Loading ${dbLabel} via ${modeLabel}`, 'loading');
+    try {
+        let payload;
+        if (currentMode === 'mistkit') {
+            payload = await postJSON('/api/records/query', {
+                recordType,
+                database: currentDatabase,
+                limit: isFinite(limit) ? limit : undefined,
+                sortBy: currentSort
+                    ? [{ field: currentSort.field, ascending: currentSort.ascending }]
+                    : undefined,
+            });
+        } else {
+            const query = { recordType };
+            if (currentSort) {
+                query.sortBy = [{
+                    fieldName: currentSort.field,
+                    ascending: currentSort.ascending,
+                }];
+            }
+            payload = await ckJsDatabase().performQuery(query, {
+                resultsLimit: isFinite(limit) ? limit : undefined,
+            });
+            if (payload && payload.hasErrors && payload.errors.length) {
+                throw new Error(payload.errors[0].reason || 'CloudKit JS query failed');
+            }
+        }
+        notes = normalizeRecords(payload);
+        if (selectedRecordName && !notes.some(n => n.recordName === selectedRecordName)) {
+            clearForm();
+        } else {
+            refreshFormState();
+            renderRows();
+        }
+        showRaw(payload);
+        setStatus(tableStatusEl, `Loaded ${notes.length} record${notes.length === 1 ? '' : 's'}.`, 'success');
+    } catch (error) {
+        setStatus(tableStatusEl, `Query failed: ${error.message}`, 'error');
+        showRaw(error.payload || { message: error.message });
+    } finally {
+        queryInFlight = false;
+        setQueryControlsDisabled(false);
+        refreshDatabasePicker();
+    }
+}
+
+function setQueryControlsDisabled(disabled) {
+    const ids = [
+        'refresh-btn', 'db-private', 'db-public',
+        'mode-mistkit', 'mode-cloudkitjs',
+        'save-btn', 'delete-btn',
+    ];
+    for (const id of ids) {
+        const el = document.getElementById(id);
+        if (el) el.disabled = disabled;
+    }
+}
+
+async function saveNote() {
+    let fields;
+    try {
+        fields = buildFields();
+    } catch (error) {
+        setStatus(formStatusEl, error.message, 'error');
+        return;
+    }
+    if (Object.keys(fields).length === 0) {
+        setStatus(formStatusEl, 'Provide a title or index.', 'error');
+        return;
+    }
+    const recordType = recordTypeInput.value.trim();
+    const note = selectedNote();
+    const isUpdate = note != null;
+    const label = isUpdate ? 'Update' : 'Create';
+    clearStatus(formStatusEl);
+    try {
+        let payload;
+        if (currentMode === 'mistkit') {
+            if (isUpdate) {
+                payload = await postJSON('/api/records/update', {
+                    recordType,
+                    database: currentDatabase,
+                    recordName: note.recordName,
+                    fields,
+                    recordChangeTag: note.recordChangeTag,
+                });
+            } else {
+                payload = await postJSON('/api/records/create', {
+                    recordType,
+                    database: currentDatabase,
+                    fields,
+                });
+            }
+        } else {
+            const record = { recordType, fields: ckJsFields(fields) };
+            if (isUpdate) {
+                record.recordName = note.recordName;
+                record.recordChangeTag = note.recordChangeTag;
+            }
+            payload = await ckJsDatabase().saveRecords([record]);
+            if (payload && payload.hasErrors && payload.errors.length) {
+                throw new Error(payload.errors[0].reason || 'CloudKit JS save failed');
+            }
+        }
+        showRaw(payload);
+        setStatus(formStatusEl, `${label} succeeded.`, 'success');
+        if (!isUpdate) clearForm();
+        if (!isUpdate) {
+            setStatus(
+                formStatusEl,
+                `Created — waiting ${REFRESH_DELAY_MS}ms for CloudKit to settle`,
+                'loading'
+            );
+            await new Promise(r => setTimeout(r, REFRESH_DELAY_MS));
+        }
+        await queryNotes();
+    } catch (error) {
+        setStatus(formStatusEl, `${label} failed: ${error.message}`, 'error');
+        showRaw(error.payload || { message: error.message });
+    }
+}
+
+async function deleteNote(note, statusEl = tableStatusEl) {
+    clearStatus(statusEl);
+    try {
+        let payload;
+        if (currentMode === 'mistkit') {
+            payload = await postJSON('/api/records/delete', {
+                recordType: note.recordType,
+                database: currentDatabase,
+                recordName: note.recordName,
+                recordChangeTag: note.recordChangeTag,
+            });
+        } else {
+            payload = await ckJsDatabase().deleteRecords([{ recordName: note.recordName }]);
+            if (payload && payload.hasErrors && payload.errors.length) {
+                throw new Error(payload.errors[0].reason || 'CloudKit JS delete failed');
+            }
+        }
+        showRaw(payload);
+        setStatus(statusEl, `Deleted ${note.recordName}.`, 'success');
+        if (note.recordName === selectedRecordName) clearForm();
+        await queryNotes();
+    } catch (error) {
+        setStatus(statusEl, `Delete failed: ${error.message}`, 'error');
+        showRaw(error.payload || { message: error.message });
+    }
+}
+
+saveBtn.addEventListener('click', saveNote);
+clearBtn.addEventListener('click', clearForm);
+deleteBtn.addEventListener('click', () => {
+    const note = selectedNote();
+    if (note) deleteNote(note, formStatusEl);
+});
+refreshBtn.addEventListener('click', queryNotes);
+
+refreshFormState();
+refreshSortIndicators();
diff --git a/Examples/MistDemo/Sources/MistDemoKit/Resources/js/assets.js b/Examples/MistDemo/Sources/MistDemoKit/Resources/js/assets.js
new file mode 100644
index 00000000..07817367
--- /dev/null
+++ b/Examples/MistDemo/Sources/MistDemoKit/Resources/js/assets.js
@@ -0,0 +1,71 @@
+// assets/rereference panel handler. The MistKit side is pending #31 —
+// the 501 stub renders the pending banner. CloudKit JS composes the
+// rereference as: fetch source → reuse CloudKit.Asset descriptor →
+// save target.
+
+const assetsStatus = document.getElementById('assets-status');
+const assetsRaw = document.getElementById('assets-raw');
+
+document.getElementById('assets-rereference-btn').addEventListener('click', async () => {
+    const source = document.getElementById('assets-source').value.trim();
+    const field = document.getElementById('assets-field').value.trim();
+    const target = document.getElementById('assets-target').value.trim();
+    const targetField = document.getElementById('assets-target-field').value.trim() || field;
+    if (!source || !field || !target) {
+        setStatus(assetsStatus, 'Provide source, asset field, and target.', 'error');
+        return;
+    }
+    if (currentMode === 'mistkit') {
+        setStatus(assetsStatus, 'Rereferencing…', 'loading');
+        try {
+            const payload = await postJSON('/api/assets/rereference', {
+                sourceRecordName: source,
+                assetField: field,
+                targetRecordName: target,
+                targetAssetField: targetField,
+            });
+            renderRaw(assetsRaw, payload);
+            if (isPendingPayload(payload)) {
+                renderPendingBanner(assetsStatus, payload);
+            } else {
+                setStatus(assetsStatus, 'Rereferenced.', 'success');
+            }
+        } catch (error) {
+            const payload = error.payload || { message: error.message };
+            renderRaw(assetsRaw, payload);
+            if (isPendingPayload(payload)) {
+                renderPendingBanner(assetsStatus, payload);
+            } else {
+                setStatus(assetsStatus, `Failed: ${error.message}`, 'error');
+            }
+        }
+        return;
+    }
+    setStatus(assetsStatus, 'Fetching source record…', 'loading');
+    try {
+        const fetchPayload = await ckJsDatabase().fetchRecords([source]);
+        if (fetchPayload.hasErrors && fetchPayload.errors.length) {
+            throw new Error(fetchPayload.errors[0].reason || 'Fetch source failed');
+        }
+        const sourceRecord = (fetchPayload.records || [])[0];
+        if (!sourceRecord) throw new Error(`Source record ${source} not found.`);
+        const assetDescriptor = sourceRecord.fields && sourceRecord.fields[field];
+        if (!assetDescriptor) {
+            throw new Error(`Field ${field} not present on source record.`);
+        }
+        setStatus(assetsStatus, 'Saving target with reused asset…', 'loading');
+        const savePayload = await ckJsDatabase().saveRecords([{
+            recordName: target,
+            recordType: sourceRecord.recordType,
+            fields: { [targetField]: assetDescriptor },
+        }]);
+        if (savePayload.hasErrors && savePayload.errors.length) {
+            throw new Error(savePayload.errors[0].reason || 'Save target failed');
+        }
+        renderRaw(assetsRaw, { fetchSource: fetchPayload, saveTarget: savePayload });
+        setStatus(assetsStatus, 'Rereferenced.', 'success');
+    } catch (error) {
+        renderRaw(assetsRaw, { message: error.message });
+        setStatus(assetsStatus, `Failed: ${error.message}`, 'error');
+    }
+});
diff --git a/Examples/MistDemo/Sources/MistDemoKit/Resources/js/auth.js b/Examples/MistDemo/Sources/MistDemoKit/Resources/js/auth.js
new file mode 100644
index 00000000..ef8ab098
--- /dev/null
+++ b/Examples/MistDemo/Sources/MistDemoKit/Resources/js/auth.js
@@ -0,0 +1,148 @@
+// CloudKit JS bootstrap + Apple ID auth flow. Polls the internal
+// `_ckSession` token from the CloudKit JS container once the user signs
+// in, then forwards it to `/api/authenticate` so the server can use it
+// for MistKit-mode requests.
+
+function setAuthed(authed) {
+    authComplete = authed;
+    document.querySelectorAll('.pre-auth').forEach(card => {
+        card.classList.toggle('pre-auth', !authed);
+    });
+    signoutButton.style.display = authed ? 'inline-block' : 'none';
+}
+
+async function loadServerConfig() {
+    const response = await fetch('/api/config');
+    if (!response.ok) throw new Error('Failed to load server config: ' + response.status);
+    return response.json();
+}
+
+async function pollWebAuthToken() {
+    const pollIntervalMs = 250;
+    const pollDeadlineMs = 10_000;
+    const pollStart = Date.now();
+    return new Promise((resolve, reject) => {
+        const handle = setInterval(() => {
+            const token = container?._auth?._ckSession;
+            if (token) {
+                clearInterval(handle);
+                resolve(token);
+                return;
+            }
+            if (Date.now() - pollStart >= pollDeadlineMs) {
+                clearInterval(handle);
+                reject(new Error('Timeout waiting for web auth token'));
+            }
+        }, pollIntervalMs);
+    });
+}
+
+async function postAuthenticate(userIdentity, token) {
+    const response = await fetch('/api/authenticate', {
+        method: 'POST',
+        headers: { 'Content-Type': 'application/json' },
+        body: JSON.stringify({
+            sessionToken: token,
+            userRecordName: userIdentity.userRecordName,
+        }),
+    });
+    if (!response.ok) {
+        throw new Error(`HTTP ${response.status}`);
+    }
+    return response.status;
+}
+
+function renderTokenDisplay(token) {
+    notesCard.style.display = 'none';
+    document.getElementById('signin-area').style.display = 'none';
+    const card = document.createElement('div');
+    card.className = 'card';
+    card.innerHTML = `
+        

Web Auth Token captured

+

Use this token for command-line CloudKit API access:

+
${token}
+

The server has shut down — you can close this window.

+ `; + document.querySelector('.layout').appendChild(card); +} + +async function handleAuthentication(userIdentity) { + if (authenticationInProgress) return; + authenticationInProgress = true; + currentUserRecordName = userIdentity?.userRecordName ?? null; + setStatus(authStatusDiv, 'Capturing web auth token...', 'success'); + try { + const token = await pollWebAuthToken(); + webAuthToken = token; + const status = await postAuthenticate(userIdentity, token); + if (status === 205) { + setStatus(authStatusDiv, 'Authentication complete.', 'success'); + renderTokenDisplay(token); + } else { + setStatus(authStatusDiv, `Authenticated as ${userIdentity.userRecordName}.`, 'success'); + setAuthed(true); + queryNotes(); + } + } catch (error) { + setStatus(authStatusDiv, `Authentication failed: ${error.message}`, 'error'); + } finally { + authenticationInProgress = false; + } +} + +signoutButton.addEventListener('click', async () => { + try { + await container.signOut(); + webAuthToken = null; + currentUserRecordName = null; + setAuthed(false); + notes = []; + clearForm(); + setStatus(authStatusDiv, 'Signed out.', 'success'); + } catch (error) { + setStatus(authStatusDiv, 'Sign out failed: ' + error.message, 'error'); + } +}); + +async function initializeCloudKit() { + try { + if (typeof CloudKit === 'undefined') { + throw new Error('CloudKit.js failed to load'); + } + const serverConfig = await loadServerConfig(); + publicDatabaseAvailable = !!serverConfig.publicDatabaseAvailable; + refreshDatabasePicker(); + CloudKit.configure({ + containers: [{ + containerIdentifier: serverConfig.containerIdentifier, + apiTokenAuth: { + apiToken: serverConfig.apiToken, + persist: true, + signInButton: { id: 'signin-button', theme: 'black' }, + }, + environment: serverConfig.environment || 'development', + }], + }); + container = CloudKit.getDefaultContainer(); + const userIdentity = await container.setUpAuth(); + if (userIdentity) { + setStatus(authStatusDiv, 'Already signed in. Capturing token...', 'success'); + await handleAuthentication(userIdentity); + } else { + setStatus(authStatusDiv, 'Click "Sign In with Apple ID" to authenticate.', 'success'); + } + container.whenUserSignsIn().then((identity) => handleAuthentication(identity)); + container.whenUserSignsOut().then(() => { + webAuthToken = null; + currentUserRecordName = null; + setAuthed(false); + notes = []; + clearForm(); + setStatus(authStatusDiv, 'Signed out.', 'success'); + }); + } catch (error) { + setStatus(authStatusDiv, 'CloudKit setup failed: ' + error.message, 'error'); + } +} + +initializeCloudKit(); diff --git a/Examples/MistDemo/Sources/MistDemoKit/Resources/js/mode.js b/Examples/MistDemo/Sources/MistDemoKit/Resources/js/mode.js new file mode 100644 index 00000000..5bff094b --- /dev/null +++ b/Examples/MistDemo/Sources/MistDemoKit/Resources/js/mode.js @@ -0,0 +1,62 @@ +// Mode + database toggles. Reads `currentMode` / `currentDatabase` / +// `publicDatabaseAvailable` set in app.js and updates the picker +// disabled-state + banner copy. + +document.getElementById('mode-mistkit').addEventListener('click', () => setMode('mistkit')); +document.getElementById('mode-cloudkitjs').addEventListener('click', () => setMode('cloudkitjs')); + +function setMode(mode) { + if (mode === currentMode) return; + currentMode = mode; + document.getElementById('mode-mistkit').classList.toggle('active', mode === 'mistkit'); + document.getElementById('mode-cloudkitjs').classList.toggle('active', mode === 'cloudkitjs'); + modeBadge.textContent = mode === 'mistkit' ? 'MistKit' : 'CloudKit JS'; + refreshDatabasePicker(); + if (authComplete) queryNotes(); +} + +dbPrivateBtn.addEventListener('click', () => setDatabase('private')); +dbPublicBtn.addEventListener('click', () => setDatabase('public')); + +function setDatabase(database) { + if (database === currentDatabase) return; + if (database === 'public' && !isPublicAllowedForCurrentMode()) { + return; + } + currentDatabase = database; + refreshDatabasePicker(); + if (authComplete) queryNotes(); +} + +function isPublicAllowedForCurrentMode() { + return currentMode === 'cloudkitjs' || publicDatabaseAvailable; +} + +function refreshDatabasePicker() { + const publicAllowed = isPublicAllowedForCurrentMode(); + dbPublicBtn.disabled = !publicAllowed; + if (!publicAllowed && currentDatabase === 'public') { + currentDatabase = 'private'; + } + dbPrivateBtn.classList.toggle('active', currentDatabase === 'private'); + dbPublicBtn.classList.toggle('active', currentDatabase === 'public'); + dbBadge.textContent = currentDatabase === 'public' ? 'Public' : 'Private'; + if (!publicDatabaseAvailable && currentMode === 'mistkit') { + dbHint.textContent = + 'MistKit + Public requires server-to-server credentials ' + + '(CLOUDKIT_KEY_ID + CLOUDKIT_PRIVATE_KEY[_PATH]). ' + + 'Restart the server with those set to enable Public on this side.'; + } else if (currentMode === 'mistkit' && currentDatabase === 'public') { + dbHint.textContent = + 'Heads-up: on MistKit + Public, records you write are owned ' + + 'by the server-to-server key, not your iCloud user — so they ' + + 'won’t carry a "You" badge.'; + } else { + dbHint.textContent = + 'Private uses the captured Apple ID web-auth token; Public ' + + 'uses server-to-server signing on the MistKit side and the ' + + 'API token on the CloudKit JS side.'; + } +} + +refreshDatabasePicker(); diff --git a/Examples/MistDemo/Sources/MistDemoKit/Resources/js/pending.js b/Examples/MistDemo/Sources/MistDemoKit/Resources/js/pending.js new file mode 100644 index 00000000..eaf534bd --- /dev/null +++ b/Examples/MistDemo/Sources/MistDemoKit/Resources/js/pending.js @@ -0,0 +1,28 @@ +// Render the standard "pending #N" banner. Used by panel modules that +// hit endpoints whose MistKit Swift wrapper hasn't landed yet — the +// server returns 501 with a JSON body containing `endpoint` + `tracking`, +// and this module formats that into a yellow advisory above the raw +// response area. + +function renderPendingBanner(statusEl, payload) { + if (!statusEl) return; + const endpoint = (payload && payload.endpoint) || 'unknown'; + const tracking = (payload && payload.tracking) || '#?'; + const msg = + `MistKit support pending for ${endpoint} — tracked in ${tracking}. ` + + `Switch to CloudKit JS mode to exercise this endpoint today.`; + statusEl.className = 'status error'; + statusEl.textContent = msg; + statusEl.style.display = 'block'; +} + +// Treat the structured 501 body as a "pending" response rather than a +// hard error so the panel surfaces the asymmetry visibly. Returns true +// if the payload is a pending-stub body. +function isPendingPayload(payload) { + return payload + && typeof payload === 'object' + && payload.error === 'not_implemented' + && typeof payload.endpoint === 'string' + && typeof payload.tracking === 'string'; +} diff --git a/Examples/MistDemo/Sources/MistDemoKit/Resources/js/records.js b/Examples/MistDemo/Sources/MistDemoKit/Resources/js/records.js new file mode 100644 index 00000000..1accc599 --- /dev/null +++ b/Examples/MistDemo/Sources/MistDemoKit/Resources/js/records.js @@ -0,0 +1,120 @@ +// records/lookup · records/changes · records/resolve panel handlers. +// records/query and records/modify are wired in app.js (Notes CRUD). + +const recordsLookupInput = document.getElementById('records-lookup-input'); +const recordsLookupStatus = document.getElementById('records-lookup-status'); +const recordsLookupRaw = document.getElementById('records-lookup-raw'); + +document.getElementById('records-lookup-btn').addEventListener('click', async () => { + const names = csv(recordsLookupInput.value); + if (names.length === 0) { + setStatus(recordsLookupStatus, 'Provide at least one record name.', 'error'); + return; + } + await runPanelOperation({ + statusEl: recordsLookupStatus, + rawEl: recordsLookupRaw, + label: 'Lookup', + fn: async () => { + if (currentMode === 'mistkit') { + // records/lookup MistKit wrapper is implemented but isn't + // exposed on the demo server yet — surface that asymmetry + // by returning a pending banner inline. + return await postJSON('/api/records/lookup', { + database: currentDatabase, + recordNames: names, + }); + } + const payload = await ckJsDatabase().fetchRecords(names); + if (payload && payload.hasErrors && payload.errors.length) { + throw new Error(payload.errors[0].reason || 'CloudKit JS lookup failed'); + } + return payload; + }, + }); +}); + +const recordsChangesZone = document.getElementById('records-changes-zone'); +const recordsChangesToken = document.getElementById('records-changes-token'); +const recordsChangesStatus = document.getElementById('records-changes-status'); +const recordsChangesRaw = document.getElementById('records-changes-raw'); + +document.getElementById('records-changes-btn').addEventListener('click', async () => { + const zoneName = recordsChangesZone.value.trim() || '_defaultZone'; + const syncToken = recordsChangesToken.value.trim() || undefined; + await runPanelOperation({ + statusEl: recordsChangesStatus, + rawEl: recordsChangesRaw, + label: 'Fetch changes', + fn: async () => { + if (currentMode === 'mistkit') { + return await postJSON('/api/records/changes', { + database: currentDatabase, + zoneName, + syncToken, + }); + } + const payload = await ckJsDatabase().fetchRecordZoneChanges({ + zoneID: { zoneName }, + syncToken, + }); + if (payload && payload.hasErrors && payload.errors.length) { + throw new Error(payload.errors[0].reason || 'CloudKit JS changes failed'); + } + return payload; + }, + }); +}); + +const recordsResolveInput = document.getElementById('records-resolve-input'); +const recordsResolveStatus = document.getElementById('records-resolve-status'); +const recordsResolveRaw = document.getElementById('records-resolve-raw'); + +document.getElementById('records-resolve-btn').addEventListener('click', async () => { + const value = recordsResolveInput.value.trim(); + if (value.length === 0) { + setStatus(recordsResolveStatus, 'Provide a record name or share URL.', 'error'); + return; + } + const isURL = /^https?:\/\//i.test(value); + if (currentMode === 'mistkit') { + // records/resolve MistKit wrapper isn't landed yet (#41). Hit the + // 501 stub to render the pending banner. + setStatus(recordsResolveStatus, 'Resolve…', 'loading'); + try { + const payload = await postJSON('/api/records/resolve', { input: value }); + renderRaw(recordsResolveRaw, payload); + if (isPendingPayload(payload)) { + renderPendingBanner(recordsResolveStatus, payload); + } else { + setStatus(recordsResolveStatus, 'Resolve succeeded.', 'success'); + } + } catch (error) { + const payload = error.payload || { message: error.message }; + renderRaw(recordsResolveRaw, payload); + if (isPendingPayload(payload)) { + renderPendingBanner(recordsResolveStatus, payload); + } else { + setStatus(recordsResolveStatus, `Resolve failed: ${error.message}`, 'error'); + } + } + return; + } + // CloudKit JS composed call — branch on input shape. + await runPanelOperation({ + statusEl: recordsResolveStatus, + rawEl: recordsResolveRaw, + label: isURL ? 'Resolve share URL' : 'Resolve record name', + fn: async () => { + if (isURL) { + const payload = await ckJsContainer().fetchShareMetadataWithURL({ shareURL: value }); + return { branch: 'shareURL', payload }; + } + const payload = await ckJsDatabase().fetchRecords([value]); + if (payload && payload.hasErrors && payload.errors.length) { + throw new Error(payload.errors[0].reason || 'CloudKit JS resolve failed'); + } + return { branch: 'recordName', payload }; + }, + }); +}); diff --git a/Examples/MistDemo/Sources/MistDemoKit/Resources/js/subscriptions.js b/Examples/MistDemo/Sources/MistDemoKit/Resources/js/subscriptions.js new file mode 100644 index 00000000..dcc4c40d --- /dev/null +++ b/Examples/MistDemo/Sources/MistDemoKit/Resources/js/subscriptions.js @@ -0,0 +1,227 @@ +// subscriptions/list · subscriptions/lookup panel handlers. MistKit +// side returns 501 (pending #49 / #50); CloudKit JS side hits the real +// browser SDK primitives. + +const subsListStatus = document.getElementById('subs-list-status'); +const subsListRaw = document.getElementById('subs-list-raw'); +const subsListTbody = document.getElementById('subs-list-tbody'); +const subsLookupStatus = document.getElementById('subs-lookup-status'); +const subsLookupRaw = document.getElementById('subs-lookup-raw'); + +// CloudKit JS `fetchAllSubscriptions` resolves to `{ subscriptions: [...] }`; +// the MistKit route (pending #49) will mirror that shape. Each subscription +// carries `subscriptionID`, `subscriptionType`, and an optional `query` +// (record-type subscriptions) and `zoneID` (zone subscriptions). +function renderSubscriptionsTable(payload) { + const subs = (payload && payload.subscriptions) || []; + renderListTable(subsListTbody, [ + s => s.subscriptionID ?? s.subscriptionId, + s => s.subscriptionType, + s => s.query && s.query.recordType, + s => s.zoneID && s.zoneID.zoneName, + ], Array.isArray(subs) ? subs : [], 'No subscriptions found.'); +} + +document.getElementById('subs-list-btn').addEventListener('click', async () => { + setStatus(subsListStatus, 'Fetching…', 'loading'); + try { + if (currentMode === 'mistkit') { + try { + const payload = await fetchJSON('/api/subscriptions'); + renderRaw(subsListRaw, payload); + if (isPendingPayload(payload)) { + renderPendingBanner(subsListStatus, payload); + } else { + renderSubscriptionsTable(payload); + setStatus(subsListStatus, 'Loaded.', 'success'); + } + } catch (error) { + const payload = error.payload || { message: error.message }; + renderRaw(subsListRaw, payload); + if (isPendingPayload(payload)) { + renderPendingBanner(subsListStatus, payload); + } else { + setStatus(subsListStatus, `Failed: ${error.message}`, 'error'); + } + } + return; + } + const payload = await ckJsDatabase().fetchAllSubscriptions(); + renderRaw(subsListRaw, payload); + renderSubscriptionsTable(payload); + setStatus(subsListStatus, 'Loaded.', 'success'); + } catch (error) { + renderRaw(subsListRaw, { message: error.message }); + setStatus(subsListStatus, `Failed: ${error.message}`, 'error'); + } +}); + +document.getElementById('subs-lookup-btn').addEventListener('click', async () => { + const ids = csv(document.getElementById('subs-lookup-input').value); + if (ids.length === 0) { + setStatus(subsLookupStatus, 'Provide at least one subscription ID.', 'error'); + return; + } + setStatus(subsLookupStatus, 'Looking up…', 'loading'); + try { + if (currentMode === 'mistkit') { + try { + const payload = await fetchJSON(`/api/subscriptions/${encodeURIComponent(ids[0])}`); + renderRaw(subsLookupRaw, payload); + if (isPendingPayload(payload)) { + renderPendingBanner(subsLookupStatus, payload); + } else { + setStatus(subsLookupStatus, 'Loaded.', 'success'); + } + } catch (error) { + const payload = error.payload || { message: error.message }; + renderRaw(subsLookupRaw, payload); + if (isPendingPayload(payload)) { + renderPendingBanner(subsLookupStatus, payload); + } else { + setStatus(subsLookupStatus, `Failed: ${error.message}`, 'error'); + } + } + return; + } + const payload = await ckJsDatabase().fetchSubscriptions(ids); + renderRaw(subsLookupRaw, payload); + setStatus(subsLookupStatus, 'Loaded.', 'success'); + } catch (error) { + renderRaw(subsLookupRaw, { message: error.message }); + setStatus(subsLookupStatus, `Failed: ${error.message}`, 'error'); + } +}); + +// ---- Create (subscriptions/modify) ---- + +const subsCreateType = document.getElementById('subs-create-type'); +const subsCreateQueryFields = document.getElementById('subs-create-query-fields'); +const subsCreateZoneFields = document.getElementById('subs-create-zone-fields'); +const subsCreateStatus = document.getElementById('subs-create-status'); +const subsCreateRaw = document.getElementById('subs-create-raw'); + +// Toggle the type-specific input row to match the selected subscription type. +function refreshSubsCreateFields() { + const isZone = subsCreateType.value === 'zone'; + subsCreateQueryFields.style.display = isZone ? 'none' : ''; + subsCreateZoneFields.style.display = isZone ? '' : 'none'; +} +subsCreateType.addEventListener('change', refreshSubsCreateFields); +refreshSubsCreateFields(); + +// Build a CloudKit.Subscription dictionary from the form. Throws with a +// user-facing message when a required field for the chosen type is missing. +function buildSubscription() { + const type = subsCreateType.value; + const id = document.getElementById('subs-create-id').value.trim(); + const subscription = { subscriptionType: type }; + if (id) subscription.subscriptionID = id; + if (type === 'zone') { + const zoneName = document.getElementById('subs-create-zone').value.trim(); + if (!zoneName) throw new Error('Provide a zone name.'); + subscription.zoneID = { zoneName }; + } else { + const recordType = document.getElementById('subs-create-record-type').value; + const firesOn = Array.from( + document.querySelectorAll('.subs-fires-on:checked') + ).map(cb => cb.value); + if (!firesOn.length) throw new Error('Select at least one "Fires on" operation.'); + subscription.firesOn = firesOn; + subscription.query = { recordType }; + } + return subscription; +} + +document.getElementById('subs-create-btn').addEventListener('click', async () => { + let subscription; + try { + subscription = buildSubscription(); + } catch (error) { + setStatus(subsCreateStatus, error.message, 'error'); + return; + } + setStatus(subsCreateStatus, 'Creating…', 'loading'); + try { + if (currentMode === 'mistkit') { + // subscriptions/modify MistKit wrapper isn't landed yet (#51) — + // hit the 501 stub and render the pending banner inline. + try { + const payload = await postJSON('/api/subscriptions/modify', subscription); + renderRaw(subsCreateRaw, payload); + if (isPendingPayload(payload)) { + renderPendingBanner(subsCreateStatus, payload); + } else { + setStatus(subsCreateStatus, 'Created.', 'success'); + } + } catch (error) { + const payload = error.payload || { message: error.message }; + renderRaw(subsCreateRaw, payload); + if (isPendingPayload(payload)) { + renderPendingBanner(subsCreateStatus, payload); + } else { + setStatus(subsCreateStatus, `Failed: ${error.message}`, 'error'); + } + } + return; + } + const payload = await ckJsDatabase().saveSubscriptions(subscription); + renderRaw(subsCreateRaw, payload); + if (payload && payload.hasErrors && payload.errors.length) { + throw new Error(payload.errors[0].reason || 'CloudKit JS save subscription failed'); + } + setStatus(subsCreateStatus, 'Created.', 'success'); + } catch (error) { + renderRaw(subsCreateRaw, error.payload || { message: error.message }); + setStatus(subsCreateStatus, `Failed: ${error.message}`, 'error'); + } +}); + +// ---- Delete (subscriptions/modify) ---- + +const subsDeleteStatus = document.getElementById('subs-delete-status'); +const subsDeleteRaw = document.getElementById('subs-delete-raw'); + +document.getElementById('subs-delete-btn').addEventListener('click', async () => { + const ids = csv(document.getElementById('subs-delete-input').value); + if (ids.length === 0) { + setStatus(subsDeleteStatus, 'Provide at least one subscription ID.', 'error'); + return; + } + setStatus(subsDeleteStatus, 'Deleting…', 'loading'); + try { + if (currentMode === 'mistkit') { + // CloudKit Web Services models delete as part of subscriptions/modify, + // whose MistKit wrapper isn't landed yet (#51) — hit the 501 stub. + try { + const payload = await postJSON('/api/subscriptions/modify', { + delete: ids.map(id => ({ subscriptionID: id })), + }); + renderRaw(subsDeleteRaw, payload); + if (isPendingPayload(payload)) { + renderPendingBanner(subsDeleteStatus, payload); + } else { + setStatus(subsDeleteStatus, 'Deleted.', 'success'); + } + } catch (error) { + const payload = error.payload || { message: error.message }; + renderRaw(subsDeleteRaw, payload); + if (isPendingPayload(payload)) { + renderPendingBanner(subsDeleteStatus, payload); + } else { + setStatus(subsDeleteStatus, `Failed: ${error.message}`, 'error'); + } + } + return; + } + const payload = await ckJsDatabase().deleteSubscriptions(ids); + renderRaw(subsDeleteRaw, payload); + if (payload && payload.hasErrors && payload.errors.length) { + throw new Error(payload.errors[0].reason || 'CloudKit JS delete subscription failed'); + } + setStatus(subsDeleteStatus, 'Deleted.', 'success'); + } catch (error) { + renderRaw(subsDeleteRaw, error.payload || { message: error.message }); + setStatus(subsDeleteStatus, `Failed: ${error.message}`, 'error'); + } +}); diff --git a/Examples/MistDemo/Sources/MistDemoKit/Resources/js/tokens.js b/Examples/MistDemo/Sources/MistDemoKit/Resources/js/tokens.js new file mode 100644 index 00000000..e8d68b0f --- /dev/null +++ b/Examples/MistDemo/Sources/MistDemoKit/Resources/js/tokens.js @@ -0,0 +1,50 @@ +// tokens/create + tokens/register panel handler. The CloudKit JS SDK +// combines token creation and registration into a single +// `container.registerForNotifications()` call (and surfaces incoming +// notifications via `addNotificationListener`). MistKit-side is pending +// #52 (create) and #53 (register) — the 501 stubs render below. + +const tokensStatus = document.getElementById('tokens-status'); +const tokensRaw = document.getElementById('tokens-raw'); + +document.getElementById('tokens-register-btn').addEventListener('click', async () => { + if (currentMode === 'mistkit') { + // Both create + register are pending. Hit both 501s sequentially and + // render the combined response so the asymmetry vs CloudKit JS is + // visible on a single panel. + setStatus(tokensStatus, 'Registering…', 'loading'); + const result = { create: null, register: null }; + try { + result.create = await postJSON('/api/tokens', {}); + } catch (error) { result.create = error.payload || { message: error.message }; } + try { + result.register = await postJSON('/api/tokens/register', {}); + } catch (error) { result.register = error.payload || { message: error.message }; } + renderRaw(tokensRaw, result); + if (isPendingPayload(result.create) || isPendingPayload(result.register)) { + renderPendingBanner(tokensStatus, result.create || result.register); + } else { + setStatus(tokensStatus, 'Registered.', 'success'); + } + return; + } + + setStatus(tokensStatus, 'Registering for notifications…', 'loading'); + try { + const result = await ckJsContainer().registerForNotifications(); + renderRaw(tokensRaw, result); + setStatus(tokensStatus, 'Registered.', 'success'); + try { + ckJsContainer().addNotificationListener((notification) => { + renderRaw(tokensRaw, { lastNotification: notification }); + }); + } catch (_listenerError) { + // addNotificationListener is best-effort; older CloudKit JS + // versions don't expose it. Don't fail the panel if the + // listener wire-up fails. + } + } catch (error) { + renderRaw(tokensRaw, { message: error.message }); + setStatus(tokensStatus, `Failed: ${error.message}`, 'error'); + } +}); diff --git a/Examples/MistDemo/Sources/MistDemoKit/Resources/js/users.js b/Examples/MistDemo/Sources/MistDemoKit/Resources/js/users.js new file mode 100644 index 00000000..054858c0 --- /dev/null +++ b/Examples/MistDemo/Sources/MistDemoKit/Resources/js/users.js @@ -0,0 +1,95 @@ +// users/caller · users/discover · users/lookup/email · users/lookup/id +// panel handlers. All four MistKit wrappers landed in #215 but aren't +// exposed on the demo server yet; CloudKit JS fully exercises every +// endpoint today. + +const usersCallerStatus = document.getElementById('users-caller-status'); +const usersCallerRaw = document.getElementById('users-caller-raw'); +const usersEmailStatus = document.getElementById('users-email-status'); +const usersEmailRaw = document.getElementById('users-email-raw'); +const usersIdStatus = document.getElementById('users-id-status'); +const usersIdRaw = document.getElementById('users-id-raw'); +const usersDiscoverStatus = document.getElementById('users-discover-status'); +const usersDiscoverRaw = document.getElementById('users-discover-raw'); + +document.getElementById('users-caller-btn').addEventListener('click', async () => { + await runPanelOperation({ + statusEl: usersCallerStatus, + rawEl: usersCallerRaw, + label: 'Fetch caller', + fn: async () => { + if (currentMode === 'mistkit') { + return await fetchJSON('/api/users/caller'); + } + return await ckJsContainer().fetchCurrentUserIdentity(); + }, + }); +}); + +document.getElementById('users-email-btn').addEventListener('click', async () => { + const email = document.getElementById('users-email-input').value.trim(); + if (!email) { + setStatus(usersEmailStatus, 'Provide an email address.', 'error'); + return; + } + await runPanelOperation({ + statusEl: usersEmailStatus, + rawEl: usersEmailRaw, + label: 'Lookup by email', + fn: async () => { + if (currentMode === 'mistkit') { + return await postJSON('/api/users/lookup/email', { emails: [email] }); + } + return await ckJsContainer().discoverUserIdentityWithEmailAddress(email); + }, + }); +}); + +document.getElementById('users-id-btn').addEventListener('click', async () => { + const recordName = document.getElementById('users-id-input').value.trim(); + if (!recordName) { + setStatus(usersIdStatus, 'Provide a user record name.', 'error'); + return; + } + await runPanelOperation({ + statusEl: usersIdStatus, + rawEl: usersIdRaw, + label: 'Lookup by record name', + fn: async () => { + if (currentMode === 'mistkit') { + return await postJSON('/api/users/lookup/id', { userRecordNames: [recordName] }); + } + return await ckJsContainer().discoverUserIdentityWithUserRecordName(recordName); + }, + }); +}); + +document.getElementById('users-discover-btn').addEventListener('click', async () => { + const emails = csv(document.getElementById('users-discover-input').value); + if (emails.length === 0) { + setStatus(usersDiscoverStatus, 'Provide at least one email.', 'error'); + return; + } + await runPanelOperation({ + statusEl: usersDiscoverStatus, + rawEl: usersDiscoverRaw, + label: 'Discover users', + fn: async () => { + if (currentMode === 'mistkit') { + return await postJSON('/api/users/discover', { emails }); + } + // CloudKit JS exposes a per-email primitive — loop and aggregate + // to match the REST endpoint's batch shape. + const results = []; + for (const email of emails) { + try { + const identity = await ckJsContainer().discoverUserIdentityWithEmailAddress(email); + results.push({ email, identity }); + } catch (error) { + results.push({ email, error: error.message }); + } + } + return { discovered: results }; + }, + }); +}); diff --git a/Examples/MistDemo/Sources/MistDemoKit/Resources/js/zones.js b/Examples/MistDemo/Sources/MistDemoKit/Resources/js/zones.js new file mode 100644 index 00000000..8c8bb555 --- /dev/null +++ b/Examples/MistDemo/Sources/MistDemoKit/Resources/js/zones.js @@ -0,0 +1,135 @@ +// zones/list · zones/lookup · zones/modify · zones/changes panel handlers. +// zones/modify is wired on the demo server (POST /api/zones/modify), so the +// MistKit-mode Create/Delete buttons hit the real CloudKitService. The other +// three (list/lookup/changes) have landed MistKit wrappers (#215, #45, #48, +// #367) but aren't yet exposed on the server, so MistKit-mode calls to them +// still 404. CloudKit JS calls are fully exercisable today. + +const zonesListStatus = document.getElementById('zones-list-status'); +const zonesListRaw = document.getElementById('zones-list-raw'); +const zonesListTbody = document.getElementById('zones-list-tbody'); +const zonesLookupStatus = document.getElementById('zones-lookup-status'); +const zonesLookupRaw = document.getElementById('zones-lookup-raw'); +const zonesModifyStatus = document.getElementById('zones-modify-status'); +const zonesModifyRaw = document.getElementById('zones-modify-raw'); +const zonesChangesStatus = document.getElementById('zones-changes-status'); +const zonesChangesRaw = document.getElementById('zones-changes-raw'); + +// Both the MistKit (`/api/zones/list`) and CloudKit JS +// (`fetchAllRecordZones`) responses wrap the zone array under `zones`; +// CloudKit JS nests the name under `zoneID.zoneName`, MistKit returns a +// flat `zoneName`, so read both. +function renderZonesTable(payload) { + const zones = (payload && payload.zones) || []; + renderListTable(zonesListTbody, [ + z => (z.zoneID && z.zoneID.zoneName) ?? z.zoneName, + z => z.zoneID && z.zoneID.ownerRecordName, + z => (z.atomic != null ? String(z.atomic) : ''), + ], Array.isArray(zones) ? zones : [], 'No zones found.'); +} + +document.getElementById('zones-list-btn').addEventListener('click', async () => { + const payload = await runPanelOperation({ + statusEl: zonesListStatus, + rawEl: zonesListRaw, + label: 'List zones', + fn: async () => { + if (currentMode === 'mistkit') { + return await postJSON('/api/zones/list', { database: currentDatabase }); + } + return await ckJsDatabase().fetchAllRecordZones(); + }, + }); + if (payload) renderZonesTable(payload); +}); + +document.getElementById('zones-lookup-btn').addEventListener('click', async () => { + const zoneNames = csv(document.getElementById('zones-lookup-input').value); + if (zoneNames.length === 0) { + setStatus(zonesLookupStatus, 'Provide at least one zone name.', 'error'); + return; + } + await runPanelOperation({ + statusEl: zonesLookupStatus, + rawEl: zonesLookupRaw, + label: 'Lookup zones', + fn: async () => { + if (currentMode === 'mistkit') { + return await postJSON('/api/zones/lookup', { + database: currentDatabase, + zoneNames, + }); + } + const zoneIDs = zoneNames.map(name => ({ zoneName: name })); + return await ckJsDatabase().fetchRecordZones(zoneIDs); + }, + }); +}); + +document.getElementById('zones-modify-create-btn').addEventListener('click', async () => { + const zoneName = document.getElementById('zones-modify-create').value.trim(); + if (zoneName.length === 0) { + setStatus(zonesModifyStatus, 'Provide a zone name to create.', 'error'); + return; + } + await runPanelOperation({ + statusEl: zonesModifyStatus, + rawEl: zonesModifyRaw, + label: 'Create zone', + fn: async () => { + if (currentMode === 'mistkit') { + return await postJSON('/api/zones/modify', { + database: currentDatabase, + create: [{ zoneName }], + }); + } + return await ckJsDatabase().saveRecordZones([{ zoneID: { zoneName } }]); + }, + }); +}); + +document.getElementById('zones-modify-delete-btn').addEventListener('click', async () => { + const zoneName = document.getElementById('zones-modify-delete').value.trim(); + if (zoneName.length === 0) { + setStatus(zonesModifyStatus, 'Provide a zone name to delete.', 'error'); + return; + } + await runPanelOperation({ + statusEl: zonesModifyStatus, + rawEl: zonesModifyRaw, + label: 'Delete zone', + fn: async () => { + if (currentMode === 'mistkit') { + return await postJSON('/api/zones/modify', { + database: currentDatabase, + delete: [{ zoneName }], + }); + } + return await ckJsDatabase().deleteRecordZones([{ zoneName }]); + }, + }); +}); + +document.getElementById('zones-changes-btn').addEventListener('click', async () => { + const token = document.getElementById('zones-changes-token').value.trim() || undefined; + await runPanelOperation({ + statusEl: zonesChangesStatus, + rawEl: zonesChangesRaw, + label: 'Zone changes', + fn: async () => { + if (currentMode === 'mistkit') { + return await postJSON('/api/zones/changes', { + database: currentDatabase, + syncToken: token, + }); + } + // CloudKit JS doesn't expose a direct zones/changes — the + // equivalent is composed per-zone via fetchRecordChanges, so + // surface that pedagogical asymmetry inline. + // CloudKit JS does expose a database-level changes primitive + // (`fetchDatabaseChanges`, returning changed record zones), which + // is the closest analog to the REST zones/changes endpoint. + return await ckJsDatabase().fetchDatabaseChanges({ syncToken: token }); + }, + }); +}); diff --git a/Examples/MistDemo/Sources/MistDemoKit/Resources/styles.css b/Examples/MistDemo/Sources/MistDemoKit/Resources/styles.css new file mode 100644 index 00000000..366c9239 --- /dev/null +++ b/Examples/MistDemo/Sources/MistDemoKit/Resources/styles.css @@ -0,0 +1,283 @@ +:root { + --bg: #f5f5f7; + --card: #ffffff; + --ink: #1d1d1f; + --muted: #6e6e73; + --accent: #0369a1; + --accent-dark: #0c4a6e; + --danger: #c00; + --danger-bg: #fdd; + --success-bg: #d1f5d3; + --success-fg: #1d5e20; + --border: #d0d7de; + --row-hover: #f0f4f8; + --row-selected: #dbeafe; +} +* { box-sizing: border-box; } +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + margin: 0; + padding: 24px; + background-color: var(--bg); + color: var(--ink); +} +.layout { + max-width: 1100px; + margin: 0 auto; + display: flex; + flex-direction: column; + gap: 16px; +} +.card { + background: var(--card); + border-radius: 12px; + padding: 24px; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.06); +} +h1 { font-size: 24px; margin: 0 0 4px; } +h2 { font-size: 18px; margin: 0 0 12px; } +h3 { font-size: 15px; margin: 0 0 8px; } +p { color: var(--muted); margin: 0 0 16px; line-height: 1.5; } +label { display: block; font-size: 13px; font-weight: 600; margin: 12px 0 4px; } +input, textarea { + width: 100%; + padding: 8px 10px; + border: 1px solid var(--border); + border-radius: 6px; + font-size: 14px; + font-family: inherit; +} +textarea { font-family: 'SF Mono', Menlo, monospace; font-size: 13px; resize: vertical; min-height: 60px; } +select { + padding: 8px 10px; + border: 1px solid var(--border); + border-radius: 6px; + font-size: 14px; + font-family: inherit; + background: #fff; +} +/* Checkboxes/radios must not inherit the full-width `input` rule above. */ +input[type="checkbox"], input[type="radio"] { width: auto; } +.field-label { display: inline-flex; flex-direction: column; gap: 4px; font-size: 12px; color: var(--muted); } +.fires-on { + display: flex; + gap: 14px; + align-items: center; + border: 1px solid var(--border); + border-radius: 6px; + padding: 6px 12px; + margin: 0; +} +.fires-on legend { font-size: 12px; color: var(--muted); padding: 0 4px; } +.fires-on label { display: inline-flex; align-items: center; gap: 5px; font-size: 13px; color: var(--ink); } +button { + background: var(--accent); + color: white; + border: none; + padding: 8px 16px; + border-radius: 6px; + cursor: pointer; + font-size: 14px; + font-weight: 600; +} +button:hover:not(:disabled) { background: var(--accent-dark); } +button:disabled { opacity: 0.45; cursor: not-allowed; } +button.secondary { + background: transparent; + color: var(--ink); + border: 1px solid var(--border); + font-weight: 500; +} +button.secondary:hover:not(:disabled) { background: var(--row-hover); } +button.danger { background: var(--danger); } +button.danger:hover:not(:disabled) { background: #900; } +.status { + margin-top: 12px; + padding: 10px 12px; + border-radius: 8px; + font-size: 13px; + display: none; +} +.status.success { background: var(--success-bg); color: var(--success-fg); display: block; } +.status.error { background: var(--danger-bg); color: var(--danger); display: block; } +.status.loading { background: var(--row-hover); color: var(--muted); display: block; } +.status.loading::after { + content: '…'; + display: inline-block; + margin-left: 4px; + animation: status-loading-dots 1.2s steps(4, end) infinite; + width: 1ch; + overflow: hidden; + vertical-align: bottom; +} +@keyframes status-loading-dots { + 0% { width: 0; } + 100% { width: 1ch; } +} +pre { + background: #0d1117; + color: #c9d1d9; + padding: 12px; + border-radius: 6px; + font-size: 12px; + overflow: auto; + margin: 8px 0 0; + max-height: 240px; + max-width: 100%; + /* Wrap long unbreakable strings (e.g. single-line JSON error payloads) + so the block never pushes past its card. */ + white-space: pre-wrap; + word-break: break-word; +} +.mode-toggle { + display: flex; + gap: 8px; + margin: 12px 0 4px; +} +.mode-toggle button { + background: transparent; + color: var(--ink); + border: 1px solid var(--border); + font-weight: 500; +} +.mode-toggle button.active { + background: var(--accent); + color: white; + border-color: var(--accent); +} +.mode-hint { font-size: 12px; color: var(--muted); margin-top: 4px; } +.notes-grid { + display: grid; + grid-template-columns: minmax(0, 1.6fr) minmax(0, 1fr); + gap: 24px; + align-items: start; +} +@media (max-width: 820px) { + .notes-grid { grid-template-columns: 1fr; } +} +.table-toolbar { + display: flex; + align-items: end; + gap: 12px; + flex-wrap: wrap; + margin-bottom: 12px; +} +.table-toolbar label { margin: 0 0 4px; } +.table-toolbar .limit-field { width: 100px; } +.table-wrap { + border: 1px solid var(--border); + border-radius: 8px; + overflow: auto; + max-height: 480px; +} +table { width: 100%; border-collapse: collapse; font-size: 13px; } +th, td { + text-align: left; + padding: 8px 10px; + border-bottom: 1px solid var(--border); +} +th { + position: sticky; + top: 0; + background: var(--row-hover); + font-weight: 600; + font-size: 12px; + text-transform: uppercase; + letter-spacing: 0.04em; + color: var(--muted); +} +th.sortable { cursor: pointer; user-select: none; white-space: nowrap; } +th.sortable:hover { color: var(--accent); } +th.sortable.active { color: var(--accent); } +.sort-indicator { display: inline-block; width: 12px; margin-left: 4px; } +td.timestamp { + font-size: 12px; + color: var(--muted); + white-space: nowrap; +} +tbody tr { cursor: pointer; } +tbody tr:hover { background: var(--row-hover); } +tbody tr.selected { background: var(--row-selected); } +td.record-name { + font-family: 'SF Mono', Menlo, monospace; + font-size: 12px; + color: var(--muted); + max-width: 180px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +td.actions { width: 1%; white-space: nowrap; } +td.actions button { padding: 4px 10px; font-size: 12px; } +.empty-state { + padding: 24px; + text-align: center; + color: var(--muted); + font-size: 13px; +} +.form-actions { + display: flex; + gap: 8px; + margin-top: 12px; + flex-wrap: wrap; +} +.form-mode-label { + font-size: 12px; + color: var(--muted); + font-weight: 500; +} +.pre-auth { opacity: 0.5; pointer-events: none; } +#signin-area { margin-top: 8px; } +.badge { display: inline-block; padding: 2px 8px; border-radius: 10px; font-size: 11px; font-weight: 600; background: #eef; color: var(--accent); } +.badge-you { background: var(--success-bg); color: var(--success-fg); margin-left: 8px; } +.raw-response summary { font-size: 12px; color: var(--muted); cursor: pointer; margin-top: 12px; } +.raw-response[open] summary { margin-bottom: 4px; } + +/* New panel styles for v1.0.0-beta.2 surface */ +.panel-grid { + display: grid; + grid-template-columns: 1fr; + gap: 12px; +} +/* Allow grid tracks to shrink below their content's intrinsic width so a + long line inside a child (e.g. a
) can't widen the whole card. */
+.panel-grid > section { min-width: 0; }
+.panel-row {
+  display: flex;
+  gap: 8px;
+  flex-wrap: wrap;
+  align-items: end;
+}
+.panel-row > input,
+.panel-row > textarea { flex: 1; min-width: 180px; }
+.composition {
+  border-left: 3px solid var(--accent);
+  padding: 8px 12px;
+  margin-top: 12px;
+  background: var(--row-hover);
+  border-radius: 4px;
+  font-size: 12px;
+  color: var(--muted);
+}
+.composition summary {
+  cursor: pointer;
+  font-weight: 600;
+  color: var(--ink);
+}
+.composition ol { margin: 6px 0 0 18px; padding: 0; }
+.composition li { margin-bottom: 2px; font-family: 'SF Mono', Menlo, monospace; }
+.endpoint-label {
+  font-family: 'SF Mono', Menlo, monospace;
+  font-size: 11px;
+  color: var(--muted);
+  margin-left: 6px;
+}
+.pending-banner {
+  background: #fffbe6;
+  border: 1px solid #ffe58f;
+  color: #ad8b00;
+  padding: 8px 12px;
+  border-radius: 6px;
+  font-size: 12px;
+  margin-top: 8px;
+}
diff --git a/Examples/MistDemo/Sources/MistDemoKit/Server/WebBackend.swift b/Examples/MistDemo/Sources/MistDemoKit/Server/WebBackend.swift
index 8fff5843..1731c941 100644
--- a/Examples/MistDemo/Sources/MistDemoKit/Server/WebBackend.swift
+++ b/Examples/MistDemo/Sources/MistDemoKit/Server/WebBackend.swift
@@ -66,6 +66,12 @@ internal protocol WebBackend: Sendable {
     recordChangeTag: String?,
     database: MistKit.Database
   ) async throws
+
+  func webModifyZones(
+    create: [String],
+    delete: [String],
+    database: MistKit.Database
+  ) async throws -> [ZoneInfo]
 }
 
 extension CloudKitService: WebBackend {
@@ -131,4 +137,15 @@ extension CloudKitService: WebBackend {
       database: database
     )
   }
+
+  internal func webModifyZones(
+    create: [String],
+    delete: [String],
+    database: MistKit.Database
+  ) async throws -> [ZoneInfo] {
+    let operations =
+      create.map { ZoneOperation.create(ZoneID(zoneName: $0)) }
+      + delete.map { ZoneOperation.delete(ZoneID(zoneName: $0)) }
+    return try await modifyZones(operations, database: database)
+  }
 }
diff --git a/Examples/MistDemo/Sources/MistDemoKit/Server/WebIndexHTML.swift b/Examples/MistDemo/Sources/MistDemoKit/Server/WebIndexHTML.swift
index 080456a3..bc62a9a0 100644
--- a/Examples/MistDemo/Sources/MistDemoKit/Server/WebIndexHTML.swift
+++ b/Examples/MistDemo/Sources/MistDemoKit/Server/WebIndexHTML.swift
@@ -32,26 +32,63 @@
 
   /// Loader for the web command's interactive page served by `WebServer`.
   ///
-  /// The HTML+JS lives in `Resources/index.html` and is read from
-  /// `Bundle.module` on first access. The mode toggle in this page lets
-  /// users compare MistKit (server-side) and CloudKit JS (browser-side)
-  /// against the same CloudKit container; the CloudKit JS side is wired
-  /// in by #329.
+  /// The HTML, CSS, and JS modules live under `Resources/` and are read
+  /// from `Bundle.module` on first access, then cached in memory so each
+  /// request serves the same `ByteBuffer`. The mode toggle in `index.html`
+  /// lets users compare MistKit (server-side) and CloudKit JS (browser-
+  /// side) against the same CloudKit container.
   internal enum WebIndexHTML {
     internal static let content: String = loadContent()
+    /// Cached extracted CSS file body served at `GET /styles.css`.
+    internal static let stylesheet: String = loadResource(
+      name: "styles", extension: "css"
+    )
+
+    /// Cached JS module bodies, keyed by the path the browser requests
+    /// (e.g. `"app.js"`). Populated once from the bundled `js/`
+    /// subdirectory; missing files surface as a preconditionFailure on
+    /// boot so a typo'd `
diff --git a/Examples/MistDemo/Sources/MistDemoKit/Resources/js/app.js b/Examples/MistDemo/Sources/MistDemoKit/Resources/js/app.js
index 31377a6d..83f575fc 100644
--- a/Examples/MistDemo/Sources/MistDemoKit/Resources/js/app.js
+++ b/Examples/MistDemo/Sources/MistDemoKit/Resources/js/app.js
@@ -43,6 +43,15 @@ const refreshBtn = document.getElementById('refresh-btn');
 const recordTypeInput = document.getElementById('record-type');
 const queryLimitInput = document.getElementById('query-limit');
 const rawResponseEl = document.getElementById('raw-response');
+const formImageGenerateBtn = document.getElementById('form-image-generate');
+const formImageClearBtn = document.getElementById('form-image-clear');
+const formImageStatusEl = document.getElementById('form-image-status');
+const formImagePreviewImg = document.getElementById('form-image-preview');
+const assetsSourceInput = document.getElementById('assets-source');
+
+// Generated image staged for the next save, or null.
+//   { dataURL, base64, blob, byteLength }
+let pendingImage = null;
 
 // ---- shared helpers ----
 
@@ -192,6 +201,83 @@ function csv(value) {
         .filter(s => s.length > 0);
 }
 
+// ---- image generation (Note.image asset) ----
+
+// Generates a 96×96 PNG with a deterministic-per-call random background and
+// the title's first character — enough variety to verify uploads/rereferences
+// distinguish between notes, without needing the user to pick a file.
+function generateNoteImage(title) {
+    const size = 96;
+    const canvas = document.createElement('canvas');
+    canvas.width = size;
+    canvas.height = size;
+    const ctx = canvas.getContext('2d');
+    const hue = Math.floor(Math.random() * 360);
+    ctx.fillStyle = `hsl(${hue}, 70%, 55%)`;
+    ctx.fillRect(0, 0, size, size);
+    ctx.fillStyle = 'white';
+    ctx.font = 'bold 56px -apple-system, BlinkMacSystemFont, sans-serif';
+    ctx.textAlign = 'center';
+    ctx.textBaseline = 'middle';
+    const initial = ((title || '').trim()[0] || '?').toUpperCase();
+    ctx.fillText(initial, size / 2, size / 2 + 2);
+    const base64 = canvas.toDataURL('image/png').split(',', 2)[1];
+    const bin = atob(base64);
+    const bytes = new Uint8Array(bin.length);
+    for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
+    const blob = new Blob([bytes], { type: 'image/png' });
+    return { base64, blob, byteLength: bytes.length };
+}
+
+// Returns the flat Asset descriptor from an image field, or null when the
+// field is missing/empty. MistKit returns `image` as a bare Asset shape;
+// CloudKit JS wraps it in `{ value: ... }`. Both formats are handled.
+function existingImageDescriptor(note) {
+    const field = note && note.raw && note.raw.fields && note.raw.fields.image;
+    if (!field) return null;
+    const value = (typeof field === 'object' && 'value' in field) ? field.value : field;
+    return (value && typeof value === 'object') ? value : null;
+}
+
+function refreshImageState() {
+    if (pendingImage) {
+        formImagePreviewImg.src = 'data:image/png;base64,' + pendingImage.base64;
+        formImagePreviewImg.style.display = 'block';
+        formImageStatusEl.textContent =
+            `Generated (${pendingImage.byteLength} bytes) — save to upload.`;
+        formImageClearBtn.disabled = false;
+        return;
+    }
+    const existing = existingImageDescriptor(selectedNote());
+    if (existing) {
+        if (existing.downloadURL) {
+            formImagePreviewImg.src = existing.downloadURL;
+            formImagePreviewImg.style.display = 'block';
+        } else {
+            formImagePreviewImg.src = '';
+            formImagePreviewImg.style.display = 'none';
+        }
+        const sizeLabel = existing.size != null ? ` (${existing.size} bytes)` : '';
+        formImageStatusEl.textContent =
+            `Existing image attached${sizeLabel} — Generate to replace.`;
+        formImageClearBtn.disabled = true;
+        return;
+    }
+    formImagePreviewImg.style.display = 'none';
+    formImagePreviewImg.src = '';
+    formImageStatusEl.textContent = 'No image attached.';
+    formImageClearBtn.disabled = true;
+}
+
+formImageGenerateBtn.addEventListener('click', () => {
+    pendingImage = generateNoteImage(titleInput.value);
+    refreshImageState();
+});
+formImageClearBtn.addEventListener('click', () => {
+    pendingImage = null;
+    refreshImageState();
+});
+
 // ---- form state for Notes CRUD ----
 
 function selectedNote() {
@@ -217,8 +303,10 @@ function clearForm() {
     selectedRecordName = null;
     titleInput.value = '';
     indexInput.value = '';
+    pendingImage = null;
     clearStatus(formStatusEl);
     refreshFormState();
+    refreshImageState();
     renderRows();
 }
 
@@ -226,8 +314,11 @@ function loadNoteIntoForm(note) {
     selectedRecordName = note.recordName;
     titleInput.value = note.title ?? '';
     indexInput.value = note.index != null ? String(note.index) : '';
+    pendingImage = null;
+    if (assetsSourceInput) assetsSourceInput.value = note.recordName;
     clearStatus(formStatusEl);
     refreshFormState();
+    refreshImageState();
     renderRows();
 }
 
@@ -476,17 +567,36 @@ async function saveNote() {
         setStatus(formStatusEl, error.message, 'error');
         return;
     }
-    if (Object.keys(fields).length === 0) {
-        setStatus(formStatusEl, 'Provide a title or index.', 'error');
-        return;
-    }
     const recordType = recordTypeInput.value.trim();
     const note = selectedNote();
     const isUpdate = note != null;
+    const hasPendingImage = pendingImage != null;
+    if (Object.keys(fields).length === 0 && !hasPendingImage) {
+        setStatus(formStatusEl, 'Provide a title, index, or image.', 'error');
+        return;
+    }
     const label = isUpdate ? 'Update' : 'Create';
     clearStatus(formStatusEl);
     try {
         let payload;
+        // MistKit asset uploads are a two-step flow: POST bytes to
+        // /api/assets/upload, then create/update with the returned descriptor.
+        // CloudKit JS handles upload inline through saveRecords by passing a
+        // Blob in the field value.
+        let uploadedRecordName = null;
+        if (hasPendingImage && currentMode === 'mistkit') {
+            setStatus(formStatusEl, 'Uploading image…', 'loading');
+            const receipt = await postJSON('/api/assets/upload', {
+                recordType,
+                fieldName: 'image',
+                recordName: isUpdate ? note.recordName : undefined,
+                database: currentDatabase,
+                data: pendingImage.base64,
+            });
+            uploadedRecordName = receipt.recordName;
+            // FieldValue's Asset case decodes the bare Asset shape directly.
+            fields.image = receipt.asset;
+        }
         if (currentMode === 'mistkit') {
             if (isUpdate) {
                 payload = await postJSON('/api/records/update', {
@@ -500,11 +610,18 @@ async function saveNote() {
                 payload = await postJSON('/api/records/create', {
                     recordType,
                     database: currentDatabase,
+                    recordName: uploadedRecordName || undefined,
                     fields,
                 });
             }
         } else {
-            const record = { recordType, fields: ckJsFields(fields) };
+            const ckFields = ckJsFields(fields);
+            if (hasPendingImage) {
+                // CloudKit JS treats Blob/File values as asset uploads and
+                // attaches them inline during saveRecords.
+                ckFields.image = { value: pendingImage.blob };
+            }
+            const record = { recordType, fields: ckFields };
             if (isUpdate) {
                 record.recordName = note.recordName;
                 record.recordChangeTag = note.recordChangeTag;
@@ -514,6 +631,7 @@ async function saveNote() {
                 throw new Error(payload.errors[0].reason || 'CloudKit JS save failed');
             }
         }
+        pendingImage = null;
         showRaw(payload);
         setStatus(formStatusEl, `${label} succeeded.`, 'success');
         if (!isUpdate) clearForm();
@@ -569,3 +687,4 @@ refreshBtn.addEventListener('click', queryNotes);
 
 refreshFormState();
 refreshSortIndicators();
+refreshImageState();
diff --git a/Examples/MistDemo/Sources/MistDemoKit/Resources/js/assets.js b/Examples/MistDemo/Sources/MistDemoKit/Resources/js/assets.js
index 07817367..8e60d38d 100644
--- a/Examples/MistDemo/Sources/MistDemoKit/Resources/js/assets.js
+++ b/Examples/MistDemo/Sources/MistDemoKit/Resources/js/assets.js
@@ -1,18 +1,24 @@
-// assets/rereference panel handler. The MistKit side is pending #31 —
-// the 501 stub renders the pending banner. CloudKit JS composes the
-// rereference as: fetch source → reuse CloudKit.Asset descriptor →
-// save target.
+// assets/rereference handler embedded in the Notes panel. The asset field
+// is fixed to `image` since the Notes schema has a single ASSET field, so
+// the UI only asks for source + target record names. MistKit POSTs to
+// /api/assets/rereference (server composes assets/rereference +
+// records/modify); CloudKit JS composes the same flow client-side: fetch
+// source → reuse CloudKit.Asset descriptor → save target.
 
+const assetsTargetInput = document.getElementById('assets-target');
 const assetsStatus = document.getElementById('assets-status');
 const assetsRaw = document.getElementById('assets-raw');
+const ASSET_FIELD = 'image';
 
 document.getElementById('assets-rereference-btn').addEventListener('click', async () => {
-    const source = document.getElementById('assets-source').value.trim();
-    const field = document.getElementById('assets-field').value.trim();
-    const target = document.getElementById('assets-target').value.trim();
-    const targetField = document.getElementById('assets-target-field').value.trim() || field;
-    if (!source || !field || !target) {
-        setStatus(assetsStatus, 'Provide source, asset field, and target.', 'error');
+    const source = document.getElementById('assets-source')?.value.trim() ?? '';
+    const target = assetsTargetInput.value.trim();
+    if (!source || !target) {
+        setStatus(assetsStatus, 'Provide both a source and a target record name.', 'error');
+        return;
+    }
+    if (source === target) {
+        setStatus(assetsStatus, 'Source and target must be different records.', 'error');
         return;
     }
     if (currentMode === 'mistkit') {
@@ -20,24 +26,18 @@ document.getElementById('assets-rereference-btn').addEventListener('click', asyn
         try {
             const payload = await postJSON('/api/assets/rereference', {
                 sourceRecordName: source,
-                assetField: field,
+                assetField: ASSET_FIELD,
                 targetRecordName: target,
-                targetAssetField: targetField,
+                targetAssetField: ASSET_FIELD,
+                database: currentDatabase,
             });
             renderRaw(assetsRaw, payload);
-            if (isPendingPayload(payload)) {
-                renderPendingBanner(assetsStatus, payload);
-            } else {
-                setStatus(assetsStatus, 'Rereferenced.', 'success');
-            }
+            setStatus(assetsStatus, `Rereferenced onto ${target}.`, 'success');
+            await queryNotes();
         } catch (error) {
             const payload = error.payload || { message: error.message };
             renderRaw(assetsRaw, payload);
-            if (isPendingPayload(payload)) {
-                renderPendingBanner(assetsStatus, payload);
-            } else {
-                setStatus(assetsStatus, `Failed: ${error.message}`, 'error');
-            }
+            setStatus(assetsStatus, `Failed: ${error.message}`, 'error');
         }
         return;
     }
@@ -49,21 +49,22 @@ document.getElementById('assets-rereference-btn').addEventListener('click', asyn
         }
         const sourceRecord = (fetchPayload.records || [])[0];
         if (!sourceRecord) throw new Error(`Source record ${source} not found.`);
-        const assetDescriptor = sourceRecord.fields && sourceRecord.fields[field];
+        const assetDescriptor = sourceRecord.fields && sourceRecord.fields[ASSET_FIELD];
         if (!assetDescriptor) {
-            throw new Error(`Field ${field} not present on source record.`);
+            throw new Error(`Field ${ASSET_FIELD} not present on source record.`);
         }
         setStatus(assetsStatus, 'Saving target with reused asset…', 'loading');
         const savePayload = await ckJsDatabase().saveRecords([{
             recordName: target,
             recordType: sourceRecord.recordType,
-            fields: { [targetField]: assetDescriptor },
+            fields: { [ASSET_FIELD]: assetDescriptor },
         }]);
         if (savePayload.hasErrors && savePayload.errors.length) {
             throw new Error(savePayload.errors[0].reason || 'Save target failed');
         }
         renderRaw(assetsRaw, { fetchSource: fetchPayload, saveTarget: savePayload });
-        setStatus(assetsStatus, 'Rereferenced.', 'success');
+        setStatus(assetsStatus, `Rereferenced onto ${target}.`, 'success');
+        await queryNotes();
     } catch (error) {
         renderRaw(assetsRaw, { message: error.message });
         setStatus(assetsStatus, `Failed: ${error.message}`, 'error');
diff --git a/Examples/MistDemo/Sources/MistDemoKit/Resources/styles.css b/Examples/MistDemo/Sources/MistDemoKit/Resources/styles.css
index 366c9239..147d7301 100644
--- a/Examples/MistDemo/Sources/MistDemoKit/Resources/styles.css
+++ b/Examples/MistDemo/Sources/MistDemoKit/Resources/styles.css
@@ -155,6 +155,31 @@ pre {
 @media (max-width: 820px) {
   .notes-grid { grid-template-columns: 1fr; }
 }
+.notes-form-stack {
+  display: flex;
+  flex-direction: column;
+  gap: 24px;
+  min-width: 0;
+}
+.notes-form-stack > section + section {
+  border-top: 1px solid var(--border);
+  padding-top: 16px;
+}
+.image-controls {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  flex-wrap: wrap;
+  margin: 4px 0 8px;
+}
+.image-state { font-size: 12px; color: var(--muted); }
+.image-preview {
+  max-width: 96px;
+  max-height: 96px;
+  border: 1px solid var(--border);
+  border-radius: 6px;
+  margin-bottom: 8px;
+}
 .table-toolbar {
   display: flex;
   align-items: end;
diff --git a/Examples/MistDemo/Sources/MistDemoKit/Server/CloudKitService+WebBackend.swift b/Examples/MistDemo/Sources/MistDemoKit/Server/CloudKitService+WebBackend.swift
index e8a4bcc7..1faa9b33 100644
--- a/Examples/MistDemo/Sources/MistDemoKit/Server/CloudKitService+WebBackend.swift
+++ b/Examples/MistDemo/Sources/MistDemoKit/Server/CloudKitService+WebBackend.swift
@@ -54,11 +54,13 @@ extension CloudKitService: WebBackend {
 
   internal func webCreate(
     recordType: String,
+    recordName: String?,
     fields: [String: FieldValue],
     database: MistKit.Database
   ) async throws -> RecordInfo {
     try await createRecord(
       recordType: recordType,
+      recordName: recordName,
       fields: fields,
       database: database
     )
@@ -154,4 +156,36 @@ extension CloudKitService: WebBackend {
       database: database
     )
   }
+
+  internal func webRereferenceAsset(
+    sourceRecordName: String,
+    assetField: String,
+    targetRecordName: String,
+    targetAssetField: String?,
+    database: MistKit.Database
+  ) async throws -> RecordInfo {
+    try await rereferenceAsset(
+      fromRecord: sourceRecordName,
+      field: assetField,
+      toRecord: targetRecordName,
+      field: targetAssetField,
+      database: database
+    )
+  }
+
+  internal func webUploadAsset(
+    data: Data,
+    recordType: String,
+    fieldName: String,
+    recordName: String?,
+    database: MistKit.Database
+  ) async throws -> AssetUploadReceipt {
+    try await uploadAssets(
+      data: data,
+      recordType: recordType,
+      fieldName: fieldName,
+      recordName: recordName,
+      database: database
+    )
+  }
 }
diff --git a/Examples/MistDemo/Sources/MistDemoKit/Server/WebBackend.swift b/Examples/MistDemo/Sources/MistDemoKit/Server/WebBackend.swift
index 1446df0d..7687cebd 100644
--- a/Examples/MistDemo/Sources/MistDemoKit/Server/WebBackend.swift
+++ b/Examples/MistDemo/Sources/MistDemoKit/Server/WebBackend.swift
@@ -48,6 +48,7 @@ internal protocol WebBackend: Sendable {
 
   func webCreate(
     recordType: String,
+    recordName: String?,
     fields: [String: FieldValue],
     database: MistKit.Database
   ) async throws -> RecordInfo
@@ -99,6 +100,22 @@ internal protocol WebBackend: Sendable {
     clientId: String?,
     database: MistKit.Database
   ) async throws
+
+  func webRereferenceAsset(
+    sourceRecordName: String,
+    assetField: String,
+    targetRecordName: String,
+    targetAssetField: String?,
+    database: MistKit.Database
+  ) async throws -> RecordInfo
+
+  func webUploadAsset(
+    data: Data,
+    recordType: String,
+    fieldName: String,
+    recordName: String?,
+    database: MistKit.Database
+  ) async throws -> AssetUploadReceipt
 }
 
 // The `CloudKitService: WebBackend` conformance lives in
diff --git a/Examples/MistDemo/Sources/MistDemoKit/Server/WebRequests+Assets.swift b/Examples/MistDemo/Sources/MistDemoKit/Server/WebRequests+Assets.swift
new file mode 100644
index 00000000..b66e739d
--- /dev/null
+++ b/Examples/MistDemo/Sources/MistDemoKit/Server/WebRequests+Assets.swift
@@ -0,0 +1,115 @@
+//
+//  WebRequests+Assets.swift
+//  MistDemo
+//
+//  Created by Leo Dion.
+//  Copyright © 2026 BrightDigit.
+//
+//  Permission is hereby granted, free of charge, to any person
+//  obtaining a copy of this software and associated documentation
+//  files (the "Software"), to deal in the Software without
+//  restriction, including without limitation the rights to use,
+//  copy, modify, merge, publish, distribute, sublicense, and/or
+//  sell copies of the Software, and to permit persons to whom the
+//  Software is furnished to do so, subject to the following
+//  conditions:
+//
+//  The above copyright notice and this permission notice shall be
+//  included in all copies or substantial portions of the Software.
+//
+//  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+//  EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+//  OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+//  NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+//  HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+//  WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+//  FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+//  OTHER DEALINGS IN THE SOFTWARE.
+//
+
+internal import Foundation
+internal import MistKit
+
+extension WebRequests {
+  /// `POST /api/assets/upload`
+  ///
+  /// Upload binary asset data to CloudKit's CDN as the first half of an
+  /// asset-bearing record write. The browser sends the bytes as a base64
+  /// string in `data`; CloudKit returns a reusable `Asset` descriptor (plus
+  /// the record name it bound the upload to) that the next
+  /// `/api/records/create` call must reference.
+  ///
+  /// `recordName` is optional — when omitted CloudKit assigns a fresh one
+  /// and echoes it back in the receipt, which the browser then forwards to
+  /// the subsequent create.
+  internal struct UploadAsset: Decodable {
+    private enum CodingKeys: String, CodingKey {
+      case recordType
+      case fieldName
+      case recordName
+      case data
+      case database
+    }
+
+    internal let recordType: String
+    internal let fieldName: String
+    internal let recordName: String?
+    internal let data: Data
+    internal let database: MistKit.Database
+
+    internal init(from decoder: any Decoder) throws {
+      let container = try decoder.container(keyedBy: CodingKeys.self)
+      self.recordType = try container.decode(String.self, forKey: .recordType)
+      self.fieldName = try container.decode(String.self, forKey: .fieldName)
+      self.recordName = try container.decodeIfPresent(
+        String.self, forKey: .recordName
+      )
+      self.data = try container.decode(Data.self, forKey: .data)
+      self.database = try WebRequests.decodeDatabase(
+        from: container, forKey: .database
+      )
+    }
+  }
+
+  /// `POST /api/assets/rereference`
+  ///
+  /// Mirrors CloudKit Web Services `assets/rereference`: re-reference an
+  /// asset that already lives on a source record onto a target record's
+  /// asset field, sharing the same underlying bytes without re-uploading.
+  /// The server composes `assets/rereference` + `records/modify` via
+  /// `CloudKitService.rereferenceAsset(fromRecord:field:toRecord:field:)`,
+  /// matching the CloudKit JS fetch-source-then-save-target flow.
+  ///
+  /// `targetAssetField` is optional — when omitted the source `assetField`
+  /// name is reused on the target, the same default as the Swift API.
+  internal struct RereferenceAsset: Decodable {
+    private enum CodingKeys: String, CodingKey {
+      case sourceRecordName
+      case assetField
+      case targetRecordName
+      case targetAssetField
+      case database
+    }
+
+    internal let sourceRecordName: String
+    internal let assetField: String
+    internal let targetRecordName: String
+    internal let targetAssetField: String?
+    internal let database: MistKit.Database
+
+    internal init(from decoder: any Decoder) throws {
+      let container = try decoder.container(keyedBy: CodingKeys.self)
+      self.sourceRecordName =
+        try container.decode(String.self, forKey: .sourceRecordName)
+      self.assetField = try container.decode(String.self, forKey: .assetField)
+      self.targetRecordName =
+        try container.decode(String.self, forKey: .targetRecordName)
+      self.targetAssetField = try container.decodeIfPresent(
+        String.self, forKey: .targetAssetField
+      )
+      self.database = try WebRequests.decodeDatabase(
+        from: container, forKey: .database
+      )
+    }
+  }
+}
diff --git a/Examples/MistDemo/Sources/MistDemoKit/Server/WebRequests.swift b/Examples/MistDemo/Sources/MistDemoKit/Server/WebRequests.swift
index 7dfc59cb..79c3706e 100644
--- a/Examples/MistDemo/Sources/MistDemoKit/Server/WebRequests.swift
+++ b/Examples/MistDemo/Sources/MistDemoKit/Server/WebRequests.swift
@@ -79,20 +79,29 @@ internal enum WebRequests {
   }
 
   /// `POST /api/records/create`
+  ///
+  /// `recordName` is optional. When the create follows an `/api/assets/upload`
+  /// the browser forwards the receipt's recordName here so CloudKit attaches
+  /// the just-uploaded bytes; otherwise CloudKit assigns a fresh name.
   internal struct Create: Decodable {
     private enum CodingKeys: String, CodingKey {
       case recordType
+      case recordName
       case fields
       case database
     }
 
     internal let recordType: String
+    internal let recordName: String?
     internal let fields: [String: FieldValue]
     internal let database: MistKit.Database
 
     internal init(from decoder: any Decoder) throws {
       let container = try decoder.container(keyedBy: CodingKeys.self)
       self.recordType = try container.decode(String.self, forKey: .recordType)
+      self.recordName = try container.decodeIfPresent(
+        String.self, forKey: .recordName
+      )
       self.fields = try container.decode(
         [String: FieldValue].self, forKey: .fields
       )
diff --git a/Examples/MistDemo/Sources/MistDemoKit/Server/WebServer+Assets.swift b/Examples/MistDemo/Sources/MistDemoKit/Server/WebServer+Assets.swift
new file mode 100644
index 00000000..a8637b2f
--- /dev/null
+++ b/Examples/MistDemo/Sources/MistDemoKit/Server/WebServer+Assets.swift
@@ -0,0 +1,90 @@
+//
+//  WebServer+Assets.swift
+//  MistDemo
+//
+//  Created by Leo Dion.
+//  Copyright © 2026 BrightDigit.
+//
+//  Permission is hereby granted, free of charge, to any person
+//  obtaining a copy of this software and associated documentation
+//  files (the "Software"), to deal in the Software without
+//  restriction, including without limitation the rights to use,
+//  copy, modify, merge, publish, distribute, sublicense, and/or
+//  sell copies of the Software, and to permit persons to whom the
+//  Software is furnished to do so, subject to the following
+//  conditions:
+//
+//  The above copyright notice and this permission notice shall be
+//  included in all copies or substantial portions of the Software.
+//
+//  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+//  EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+//  OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+//  NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+//  HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+//  WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+//  FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+//  OTHER DEALINGS IN THE SOFTWARE.
+//
+
+#if canImport(Hummingbird)
+  internal import Foundation
+  internal import Hummingbird
+  internal import MistKit
+
+  extension WebServer {
+    /// `POST /api/assets/upload` and `POST /api/assets/rereference` — back
+    /// the Notes panel's image generator (upload) and the rereference
+    /// sub-section. CloudKit JS mode handles uploads inline through
+    /// `saveRecords` and composes rereference client-side (fetch source →
+    /// reuse descriptor → save target).
+    internal func addAssetEndpoints(
+      api: RouterGroup
+    ) {
+      let tokenStore = self.tokenStore
+      let backendFactory = self.backendFactory
+
+      api.post("assets/upload") { request, context -> Response in
+        guard let token = await tokenStore.currentToken else {
+          return Response(status: .unauthorized)
+        }
+        let body = try await request.decode(
+          as: WebRequests.UploadAsset.self, context: context
+        )
+        return try await Self.runOperation { () -> Data in
+          let backend = try backendFactory.make(token)
+          let receipt = try await backend.webUploadAsset(
+            data: body.data,
+            recordType: body.recordType,
+            fieldName: body.fieldName,
+            recordName: body.recordName,
+            database: body.database
+          )
+          return try WebJSON.encoder().encode(receipt)
+        }
+      }
+
+      api.post("assets/rereference") { request, context -> Response in
+        guard let token = await tokenStore.currentToken else {
+          return Response(status: .unauthorized)
+        }
+        let body = try await request.decode(
+          as: WebRequests.RereferenceAsset.self, context: context
+        )
+        return try await Self.runOperation { () -> Data in
+          let backend = try backendFactory.make(token)
+          let record = try await backend.webRereferenceAsset(
+            sourceRecordName: body.sourceRecordName,
+            assetField: body.assetField,
+            targetRecordName: body.targetRecordName,
+            targetAssetField: body.targetAssetField,
+            database: body.database
+          )
+          return try WebJSON.encoder().encode(
+            WebResponse.Records(records: [record])
+          )
+        }
+      }
+    }
+  }
+#endif
diff --git a/Examples/MistDemo/Sources/MistDemoKit/Server/WebServer+CRUD.swift b/Examples/MistDemo/Sources/MistDemoKit/Server/WebServer+CRUD.swift
index 998763cc..1591b9e9 100644
--- a/Examples/MistDemo/Sources/MistDemoKit/Server/WebServer+CRUD.swift
+++ b/Examples/MistDemo/Sources/MistDemoKit/Server/WebServer+CRUD.swift
@@ -76,6 +76,7 @@
           let backend = try backendFactory.make(token)
           let record = try await backend.webCreate(
             recordType: body.recordType,
+            recordName: body.recordName,
             fields: body.fields,
             database: body.database
           )
diff --git a/Examples/MistDemo/Sources/MistDemoKit/Server/WebServer+Pending.swift b/Examples/MistDemo/Sources/MistDemoKit/Server/WebServer+Pending.swift
index ad19d9e5..e598f975 100644
--- a/Examples/MistDemo/Sources/MistDemoKit/Server/WebServer+Pending.swift
+++ b/Examples/MistDemo/Sources/MistDemoKit/Server/WebServer+Pending.swift
@@ -71,12 +71,29 @@
       }
     }
 
-    /// Register 501 stubs for every CloudKit Web Services endpoint whose
-    /// MistKit Swift wrapper hasn't landed yet. Each route returns the
-    /// shared `PendingStub.responseJSON` payload so the browser-side panel
-    /// renders a structured "pending #N" body. When a tracking issue lands,
-    /// flip the corresponding `api.(...)` registration here to the
-    /// real handler (or move it into a dedicated extension file).
+    /// Register 501 stubs for every CloudKit Web Services endpoint not yet
+    /// wired to a real handler. Each route returns the shared
+    /// `PendingStub.responseJSON` payload so the browser-side panel renders a
+    /// structured "pending #N" body. When a route is ready, flip the
+    /// corresponding `api.(...)` registration here to the real handler
+    /// (or move it into a dedicated extension file).
+    ///
+    /// Remaining pending calls fall into two groups:
+    ///
+    /// 1. **No MistKit wrapper yet** (registered below):
+    ///    - `POST records/resolve` (#41) — `CloudKitService` has no
+    ///      `resolveRecords`; `ResolveCommand` likewise only prints a stub.
+    ///
+    /// 2. **Wrapper landed, route wiring outstanding** (#394, registered in
+    ///    ``addUnwiredLandedEndpoints(api:)``): `records/lookup`,
+    ///    `records/changes`, `zones/list`, `zones/lookup`, `zones/changes`,
+    ///    `users/caller`, `users/discover`, `users/lookup/email`,
+    ///    `users/lookup/id`.
+    ///
+    /// Already moved off this list to real handlers: `subscriptions/*`
+    /// (#49/#50/#51 → `WebServer+Subscriptions`), `tokens/*`
+    /// (#52/#53 → `WebServer+Tokens`), and `assets/rereference`
+    /// (#31 → `WebServer+Assets`).
     internal func addPendingEndpoints(
       api: RouterGroup
     ) {
@@ -87,32 +104,24 @@
         endpoint: "records/resolve",
         trackingIssue: 41
       )
-      Self.registerPending(
-        api: api,
-        verb: .post,
-        path: "assets/rereference",
-        endpoint: "assets/rereference",
-        trackingIssue: 31
-      )
-      // subscriptions/* (#49/#50/#51) and tokens/* (#52/#53) are now wired to
-      // real MistKit handlers — see `WebServer+Subscriptions` / `WebServer+Tokens`.
 
       addUnwiredLandedEndpoints(api: api)
     }
 
     /// Register 501 stubs for endpoints whose MistKit Swift wrapper *has*
     /// already landed but isn't exposed on the demo server yet — only the
-    /// `/api/*` route wiring is outstanding, tracked by #370. These return
+    /// `/api/*` route wiring is outstanding, tracked by #394. These return
     /// the same structured pending body as `addPendingEndpoints`; replacing a
     /// stub here with a real handler (mirroring `WebServer+Zones`) is the
-    /// remaining work for #370's "exercisable in both modes" criterion.
+    /// remaining work for #394's "exercisable in both modes" criterion.
     ///
-    /// Note: #370 is the tracking issue for the *route wiring*, not for the
-    /// wrapper (which shipped under #215/#45/#47/#48/#367).
+    /// Note: #394 is the tracking issue for the *route wiring*, not for the
+    /// wrapper (which shipped under #215/#45/#47/#48/#367); #370 only
+    /// scaffolded these as stubs.
     internal func addUnwiredLandedEndpoints(
       api: RouterGroup
     ) {
-      let routeWiringIssue = 370
+      let routeWiringIssue = 394
       // Each route's `endpoint` label equals its path here, so a flat list of
       // (verb, path) pairs drives registration without per-route boilerplate.
       let routes: [(verb: PendingVerb, path: String)] = [
diff --git a/Examples/MistDemo/Sources/MistDemoKit/Server/WebServer.swift b/Examples/MistDemo/Sources/MistDemoKit/Server/WebServer.swift
index 9fe87ae9..db4c491e 100644
--- a/Examples/MistDemo/Sources/MistDemoKit/Server/WebServer.swift
+++ b/Examples/MistDemo/Sources/MistDemoKit/Server/WebServer.swift
@@ -130,6 +130,7 @@
       addZonesModifyEndpoint(api: api)
       addSubscriptionEndpoints(api: api)
       addTokenEndpoints(api: api)
+      addAssetEndpoints(api: api)
       addPendingEndpoints(api: api)
 
       return router
diff --git a/Examples/MistDemo/Tests/MistDemoTests/Server/MockBackend+Calls.swift b/Examples/MistDemo/Tests/MistDemoTests/Server/MockBackend+Calls.swift
index 68503929..0bc57401 100644
--- a/Examples/MistDemo/Tests/MistDemoTests/Server/MockBackend+Calls.swift
+++ b/Examples/MistDemo/Tests/MistDemoTests/Server/MockBackend+Calls.swift
@@ -28,6 +28,7 @@
 //
 
 #if canImport(Hummingbird)
+  internal import Foundation
   internal import MistKit
 
   @testable import MistDemoKit
@@ -44,6 +45,7 @@
     /// Captured arguments from the most recent `webCreate` call.
     internal struct CreateCall: Sendable {
       internal let recordType: String
+      internal let recordName: String?
       internal let fields: [String: String]
       internal let database: MistKit.Database
     }
@@ -98,5 +100,23 @@
       internal let clientId: String?
       internal let database: MistKit.Database
     }
+
+    /// Captured arguments from the most recent `webRereferenceAsset` call.
+    internal struct RereferenceAssetCall: Sendable {
+      internal let sourceRecordName: String
+      internal let assetField: String
+      internal let targetRecordName: String
+      internal let targetAssetField: String?
+      internal let database: MistKit.Database
+    }
+
+    /// Captured arguments from the most recent `webUploadAsset` call.
+    internal struct UploadAssetCall: Sendable {
+      internal let data: Data
+      internal let recordType: String
+      internal let fieldName: String
+      internal let recordName: String?
+      internal let database: MistKit.Database
+    }
   }
 #endif
diff --git a/Examples/MistDemo/Tests/MistDemoTests/Server/MockBackend+RecordOperations.swift b/Examples/MistDemo/Tests/MistDemoTests/Server/MockBackend+RecordOperations.swift
new file mode 100644
index 00000000..a5629477
--- /dev/null
+++ b/Examples/MistDemo/Tests/MistDemoTests/Server/MockBackend+RecordOperations.swift
@@ -0,0 +1,123 @@
+//
+//  MockBackend+RecordOperations.swift
+//  MistDemoTests
+//
+//  Created by Leo Dion.
+//  Copyright © 2026 BrightDigit.
+//
+//  Permission is hereby granted, free of charge, to any person
+//  obtaining a copy of this software and associated documentation
+//  files (the "Software"), to deal in the Software without
+//  restriction, including without limitation the rights to use,
+//  copy, modify, merge, publish, distribute, sublicense, and/or
+//  sell copies of the Software, and to permit persons to whom the
+//  Software is furnished to do so, subject to the following
+//  conditions:
+//
+//  The above copyright notice and this permission notice shall be
+//  included in all copies or substantial portions of the Software.
+//
+//  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+//  EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+//  OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+//  NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+//  HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+//  WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+//  FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+//  OTHER DEALINGS IN THE SOFTWARE.
+//
+
+#if canImport(Hummingbird)
+  internal import MistKit
+
+  @testable import MistDemoKit
+
+  extension MockBackend {
+    internal func webQuery(
+      recordType: String,
+      limit: Int?,
+      sortBy: [WebRequests.QuerySortField]?,
+      database: MistKit.Database
+    ) async throws -> [RecordInfo] {
+      lastQuery = QueryCall(
+        recordType: recordType,
+        limit: limit,
+        sortBy: sortBy,
+        database: database
+      )
+      try consumePendingError()
+      return [
+        Self.stubRecord(recordType: recordType, recordName: "stub-1")
+      ]
+    }
+
+    internal func webCreate(
+      recordType: String,
+      recordName: String?,
+      fields: [String: FieldValue],
+      database: MistKit.Database
+    ) async throws -> RecordInfo {
+      lastCreate = CreateCall(
+        recordType: recordType,
+        recordName: recordName,
+        fields: Self.flatten(fields),
+        database: database
+      )
+      try consumePendingError()
+      return Self.stubRecord(
+        recordType: recordType, recordName: recordName ?? "created-1"
+      )
+    }
+
+    internal func webUpdate(
+      recordType: String,
+      recordName: String,
+      fields: [String: FieldValue],
+      recordChangeTag: String?,
+      database: MistKit.Database
+    ) async throws -> RecordInfo {
+      lastUpdate = UpdateCall(
+        recordType: recordType,
+        recordName: recordName,
+        fields: Self.flatten(fields),
+        recordChangeTag: recordChangeTag,
+        database: database
+      )
+      try consumePendingError()
+      return Self.stubRecord(
+        recordType: recordType, recordName: recordName
+      )
+    }
+
+    internal func webDelete(
+      recordType: String,
+      recordName: String,
+      recordChangeTag: String?,
+      database: MistKit.Database
+    ) async throws {
+      lastDelete = DeleteCall(
+        recordType: recordType,
+        recordName: recordName,
+        recordChangeTag: recordChangeTag,
+        database: database
+      )
+      try consumePendingError()
+    }
+
+    internal func webModifyZones(
+      create: [String],
+      delete: [String],
+      database: MistKit.Database
+    ) async throws -> [ZoneInfo] {
+      lastModifyZones = ModifyZonesCall(
+        create: create,
+        delete: delete,
+        database: database
+      )
+      try consumePendingError()
+      return create.map { name in
+        ZoneInfo(zoneName: name, ownerRecordName: nil, capabilities: [])
+      }
+    }
+  }
+#endif
diff --git a/Examples/MistDemo/Tests/MistDemoTests/Server/MockBackend+ServiceOperations.swift b/Examples/MistDemo/Tests/MistDemoTests/Server/MockBackend+ServiceOperations.swift
new file mode 100644
index 00000000..0f586ca0
--- /dev/null
+++ b/Examples/MistDemo/Tests/MistDemoTests/Server/MockBackend+ServiceOperations.swift
@@ -0,0 +1,157 @@
+//
+//  MockBackend+ServiceOperations.swift
+//  MistDemoTests
+//
+//  Created by Leo Dion.
+//  Copyright © 2026 BrightDigit.
+//
+//  Permission is hereby granted, free of charge, to any person
+//  obtaining a copy of this software and associated documentation
+//  files (the "Software"), to deal in the Software without
+//  restriction, including without limitation the rights to use,
+//  copy, modify, merge, publish, distribute, sublicense, and/or
+//  sell copies of the Software, and to permit persons to whom the
+//  Software is furnished to do so, subject to the following
+//  conditions:
+//
+//  The above copyright notice and this permission notice shall be
+//  included in all copies or substantial portions of the Software.
+//
+//  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+//  EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+//  OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+//  NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+//  HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+//  WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+//  FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+//  OTHER DEALINGS IN THE SOFTWARE.
+//
+
+#if canImport(Hummingbird)
+  internal import Foundation
+  internal import MistKit
+
+  @testable import MistDemoKit
+
+  extension MockBackend {
+    internal func webListSubscriptions(
+      database: MistKit.Database
+    ) async throws -> [SubscriptionInfo] {
+      didListSubscriptions = true
+      try consumePendingError()
+      return stubSubscriptions
+    }
+
+    internal func webLookupSubscriptions(
+      ids: [String],
+      database: MistKit.Database
+    ) async throws -> [SubscriptionInfo] {
+      lastLookupSubscriptions = LookupSubscriptionsCall(ids: ids, database: database)
+      try consumePendingError()
+      return stubSubscriptions.filter { ids.contains($0.subscriptionID) }
+    }
+
+    internal func webModifySubscriptions(
+      operations: [SubscriptionOperation],
+      database: MistKit.Database
+    ) async throws -> [SubscriptionInfo] {
+      lastModifySubscriptions = ModifySubscriptionsCall(
+        operations: operations, database: database
+      )
+      try consumePendingError()
+      return operations.compactMap { operation in
+        if case .create(let info) = operation {
+          return info
+        }
+        return nil
+      }
+    }
+
+    internal func webCreateToken(
+      environment: APNsEnvironment,
+      clientId: String?,
+      database: MistKit.Database
+    ) async throws -> APNsTokenResult {
+      lastCreateToken = CreateTokenCall(
+        environment: environment,
+        clientId: clientId,
+        database: database
+      )
+      try consumePendingError()
+      guard let stubURL = URL(string: "https://stub.example/webcourier") else {
+        struct InvalidStubURL: Error {}
+        throw InvalidStubURL()
+      }
+      return APNsTokenResult(
+        environment: environment,
+        apnsToken: "stub-apns",
+        webcourierURL: stubURL
+      )
+    }
+
+    internal func webRegisterToken(
+      apnsToken: String,
+      environment: APNsEnvironment,
+      clientId: String?,
+      database: MistKit.Database
+    ) async throws {
+      lastRegisterToken = RegisterTokenCall(
+        apnsToken: apnsToken,
+        environment: environment,
+        clientId: clientId,
+        database: database
+      )
+      try consumePendingError()
+    }
+
+    internal func webRereferenceAsset(
+      sourceRecordName: String,
+      assetField: String,
+      targetRecordName: String,
+      targetAssetField: String?,
+      database: MistKit.Database
+    ) async throws -> RecordInfo {
+      lastRereferenceAsset = RereferenceAssetCall(
+        sourceRecordName: sourceRecordName,
+        assetField: assetField,
+        targetRecordName: targetRecordName,
+        targetAssetField: targetAssetField,
+        database: database
+      )
+      try consumePendingError()
+      return Self.stubRecord(
+        recordType: "Note", recordName: targetRecordName
+      )
+    }
+
+    internal func webUploadAsset(
+      data: Data,
+      recordType: String,
+      fieldName: String,
+      recordName: String?,
+      database: MistKit.Database
+    ) async throws -> AssetUploadReceipt {
+      lastUploadAsset = UploadAssetCall(
+        data: data,
+        recordType: recordType,
+        fieldName: fieldName,
+        recordName: recordName,
+        database: database
+      )
+      try consumePendingError()
+      let assignedName = recordName ?? "stub-upload-\(data.count)"
+      return AssetUploadReceipt(
+        asset: Asset(
+          fileChecksum: "stub-checksum",
+          size: Int64(data.count),
+          referenceChecksum: nil,
+          wrappingKey: nil,
+          receipt: "stub-receipt",
+          downloadURL: nil
+        ),
+        recordName: assignedName,
+        fieldName: fieldName
+      )
+    }
+  }
+#endif
diff --git a/Examples/MistDemo/Tests/MistDemoTests/Server/MockBackend.swift b/Examples/MistDemo/Tests/MistDemoTests/Server/MockBackend.swift
index 9be5e842..480f7850 100644
--- a/Examples/MistDemo/Tests/MistDemoTests/Server/MockBackend.swift
+++ b/Examples/MistDemo/Tests/MistDemoTests/Server/MockBackend.swift
@@ -35,21 +35,29 @@
 
   /// In-memory `WebBackend` for routing-level tests. Records the last
   /// call to each operation and returns deterministic stub records.
+  ///
+  /// The `WebBackend` conformance is split across extension files —
+  /// `MockBackend+RecordOperations` (records/zones) and
+  /// `MockBackend+ServiceOperations` (subscriptions/tokens/assets) — so the
+  /// recorded-call properties below are `internal` (rather than
+  /// `private(set)`) to let those extensions record into them.
   internal final actor MockBackend: WebBackend {
-    internal private(set) var lastQuery: QueryCall?
-    internal private(set) var lastCreate: CreateCall?
-    internal private(set) var lastUpdate: UpdateCall?
-    internal private(set) var lastDelete: DeleteCall?
-    internal private(set) var lastModifyZones: ModifyZonesCall?
-    internal private(set) var didListSubscriptions = false
-    internal private(set) var lastLookupSubscriptions: LookupSubscriptionsCall?
-    internal private(set) var lastModifySubscriptions: ModifySubscriptionsCall?
-    internal private(set) var lastCreateToken: CreateTokenCall?
-    internal private(set) var lastRegisterToken: RegisterTokenCall?
+    internal var lastQuery: QueryCall?
+    internal var lastCreate: CreateCall?
+    internal var lastUpdate: UpdateCall?
+    internal var lastDelete: DeleteCall?
+    internal var lastModifyZones: ModifyZonesCall?
+    internal var didListSubscriptions = false
+    internal var lastLookupSubscriptions: LookupSubscriptionsCall?
+    internal var lastModifySubscriptions: ModifySubscriptionsCall?
+    internal var lastCreateToken: CreateTokenCall?
+    internal var lastRegisterToken: RegisterTokenCall?
+    internal var lastRereferenceAsset: RereferenceAssetCall?
+    internal var lastUploadAsset: UploadAssetCall?
     private var pendingError: String?
 
     /// Stub subscriptions (tests can seed); defaults to one query subscription.
-    private var stubSubscriptions: [SubscriptionInfo] = [
+    internal var stubSubscriptions: [SubscriptionInfo] = [
       .query(subscriptionID: "stub-sub", recordType: "Note", firesOn: [.create])
     ]
 
@@ -57,162 +65,9 @@
       pendingError = message
     }
 
-    internal func webQuery(
-      recordType: String,
-      limit: Int?,
-      sortBy: [WebRequests.QuerySortField]?,
-      database: MistKit.Database
-    ) async throws -> [RecordInfo] {
-      lastQuery = QueryCall(
-        recordType: recordType,
-        limit: limit,
-        sortBy: sortBy,
-        database: database
-      )
-      try consumePendingError()
-      return [
-        Self.stubRecord(recordType: recordType, recordName: "stub-1")
-      ]
-    }
-
-    internal func webCreate(
-      recordType: String,
-      fields: [String: FieldValue],
-      database: MistKit.Database
-    ) async throws -> RecordInfo {
-      lastCreate = CreateCall(
-        recordType: recordType,
-        fields: Self.flatten(fields),
-        database: database
-      )
-      try consumePendingError()
-      return Self.stubRecord(
-        recordType: recordType, recordName: "created-1"
-      )
-    }
-
-    internal func webUpdate(
-      recordType: String,
-      recordName: String,
-      fields: [String: FieldValue],
-      recordChangeTag: String?,
-      database: MistKit.Database
-    ) async throws -> RecordInfo {
-      lastUpdate = UpdateCall(
-        recordType: recordType,
-        recordName: recordName,
-        fields: Self.flatten(fields),
-        recordChangeTag: recordChangeTag,
-        database: database
-      )
-      try consumePendingError()
-      return Self.stubRecord(
-        recordType: recordType, recordName: recordName
-      )
-    }
-
-    internal func webDelete(
-      recordType: String,
-      recordName: String,
-      recordChangeTag: String?,
-      database: MistKit.Database
-    ) async throws {
-      lastDelete = DeleteCall(
-        recordType: recordType,
-        recordName: recordName,
-        recordChangeTag: recordChangeTag,
-        database: database
-      )
-      try consumePendingError()
-    }
-
-    internal func webModifyZones(
-      create: [String],
-      delete: [String],
-      database: MistKit.Database
-    ) async throws -> [ZoneInfo] {
-      lastModifyZones = ModifyZonesCall(
-        create: create,
-        delete: delete,
-        database: database
-      )
-      try consumePendingError()
-      return create.map { name in
-        ZoneInfo(zoneName: name, ownerRecordName: nil, capabilities: [])
-      }
-    }
-
-    internal func webListSubscriptions(
-      database: MistKit.Database
-    ) async throws -> [SubscriptionInfo] {
-      didListSubscriptions = true
-      try consumePendingError()
-      return stubSubscriptions
-    }
-
-    internal func webLookupSubscriptions(
-      ids: [String],
-      database: MistKit.Database
-    ) async throws -> [SubscriptionInfo] {
-      lastLookupSubscriptions = LookupSubscriptionsCall(ids: ids, database: database)
-      try consumePendingError()
-      return stubSubscriptions.filter { ids.contains($0.subscriptionID) }
-    }
-
-    internal func webModifySubscriptions(
-      operations: [SubscriptionOperation],
-      database: MistKit.Database
-    ) async throws -> [SubscriptionInfo] {
-      lastModifySubscriptions = ModifySubscriptionsCall(
-        operations: operations, database: database
-      )
-      try consumePendingError()
-      return operations.compactMap { operation in
-        if case .create(let info) = operation {
-          return info
-        }
-        return nil
-      }
-    }
-
-    internal func webCreateToken(
-      environment: APNsEnvironment,
-      clientId: String?,
-      database: MistKit.Database
-    ) async throws -> APNsTokenResult {
-      lastCreateToken = CreateTokenCall(
-        environment: environment,
-        clientId: clientId,
-        database: database
-      )
-      try consumePendingError()
-      guard let stubURL = URL(string: "https://stub.example/webcourier") else {
-        struct InvalidStubURL: Error {}
-        throw InvalidStubURL()
-      }
-      return APNsTokenResult(
-        environment: environment,
-        apnsToken: "stub-apns",
-        webcourierURL: stubURL
-      )
-    }
-
-    internal func webRegisterToken(
-      apnsToken: String,
-      environment: APNsEnvironment,
-      clientId: String?,
-      database: MistKit.Database
-    ) async throws {
-      lastRegisterToken = RegisterTokenCall(
-        apnsToken: apnsToken,
-        environment: environment,
-        clientId: clientId,
-        database: database
-      )
-      try consumePendingError()
-    }
-
-    private func consumePendingError() throws {
+    /// Throw the seeded error (if any) once, then clear it. Called at the top
+    /// of every conformance method in the operation extensions.
+    internal func consumePendingError() throws {
       if let message = pendingError {
         pendingError = nil
         struct StubError: LocalizedError {
diff --git a/Examples/MistDemo/Tests/MistDemoTests/Server/WebServerTests+Assets.swift b/Examples/MistDemo/Tests/MistDemoTests/Server/WebServerTests+Assets.swift
new file mode 100644
index 00000000..e9dafc29
--- /dev/null
+++ b/Examples/MistDemo/Tests/MistDemoTests/Server/WebServerTests+Assets.swift
@@ -0,0 +1,135 @@
+//
+//  WebServerTests+Assets.swift
+//  MistDemoTests
+//
+//  Created by Leo Dion.
+//  Copyright © 2026 BrightDigit.
+//
+//  Permission is hereby granted, free of charge, to any person
+//  obtaining a copy of this software and associated documentation
+//  files (the "Software"), to deal in the Software without
+//  restriction, including without limitation the rights to use,
+//  copy, modify, merge, publish, distribute, sublicense, and/or
+//  sell copies of the Software, and to permit persons to whom the
+//  Software is furnished to do so, subject to the following
+//  conditions:
+//
+//  The above copyright notice and this permission notice shall be
+//  included in all copies or substantial portions of the Software.
+//
+//  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+//  EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+//  OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+//  NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+//  HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+//  WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+//  FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+//  OTHER DEALINGS IN THE SOFTWARE.
+//
+
+#if canImport(Hummingbird)
+  internal import Foundation
+  internal import HTTPTypes
+  internal import Hummingbird
+  internal import HummingbirdTesting
+  internal import MistKit
+  internal import Testing
+
+  @testable import MistDemoKit
+
+  extension WebServerTests {
+    @Test("POST /api/assets/rereference forwards source/target to the backend")
+    internal func assetsRereferenceForwards() async throws {
+      let fixture = Self.makeFixture(authenticated: true)
+      let app = Application(router: try fixture.server.makeRouter())
+      let jsonBody = """
+        {"database":"private","sourceRecordName":"src-1","assetField":"image",\
+        "targetRecordName":"tgt-1","targetAssetField":"image"}
+        """
+
+      try await app.test(.router) { client in
+        try await client.execute(
+          uri: "/api/assets/rereference",
+          method: .post,
+          headers: [.contentType: "application/json"],
+          body: ByteBuffer(string: jsonBody)
+        ) { response in
+          #expect(response.status == .ok)
+        }
+      }
+
+      let captured = await fixture.backend.lastRereferenceAsset
+      #expect(captured?.sourceRecordName == "src-1")
+      #expect(captured?.assetField == "image")
+      #expect(captured?.targetRecordName == "tgt-1")
+      #expect(captured?.targetAssetField == "image")
+      #expect(captured?.database == .private)
+    }
+
+    @Test("POST /api/assets/rereference returns 401 without a captured auth token")
+    internal func assetsRereferenceRequiresAuth() async throws {
+      let fixture = Self.makeFixture(authenticated: false)
+      let app = Application(router: try fixture.server.makeRouter())
+
+      try await app.test(.router) { client in
+        try await client.execute(
+          uri: "/api/assets/rereference",
+          method: .post,
+          headers: [.contentType: "application/json"],
+          body: ByteBuffer(
+            string: #"{"sourceRecordName":"a","assetField":"image","targetRecordName":"b"}"#
+          )
+        ) { response in
+          #expect(response.status == .unauthorized)
+        }
+      }
+    }
+
+    @Test("POST /api/assets/upload forwards bytes and metadata to the backend")
+    internal func assetsUploadForwards() async throws {
+      let fixture = Self.makeFixture(authenticated: true)
+      let app = Application(router: try fixture.server.makeRouter())
+      let payloadBytes = Data([0xDE, 0xAD, 0xBE, 0xEF])
+      let base64 = payloadBytes.base64EncodedString()
+      let jsonBody =
+        #"{"database":"private","recordType":"Note","fieldName":"image","data":"\#(base64)"}"#
+
+      try await app.test(.router) { client in
+        try await client.execute(
+          uri: "/api/assets/upload",
+          method: .post,
+          headers: [.contentType: "application/json"],
+          body: ByteBuffer(string: jsonBody)
+        ) { response in
+          #expect(response.status == .ok)
+        }
+      }
+
+      let captured = await fixture.backend.lastUploadAsset
+      #expect(captured?.recordType == "Note")
+      #expect(captured?.fieldName == "image")
+      #expect(captured?.recordName == nil)
+      #expect(captured?.data == payloadBytes)
+      #expect(captured?.database == .private)
+    }
+
+    @Test("POST /api/assets/upload returns 401 without a captured auth token")
+    internal func assetsUploadRequiresAuth() async throws {
+      let fixture = Self.makeFixture(authenticated: false)
+      let app = Application(router: try fixture.server.makeRouter())
+
+      try await app.test(.router) { client in
+        try await client.execute(
+          uri: "/api/assets/upload",
+          method: .post,
+          headers: [.contentType: "application/json"],
+          body: ByteBuffer(
+            string: #"{"recordType":"Note","fieldName":"image","data":"AA=="}"#
+          )
+        ) { response in
+          #expect(response.status == .unauthorized)
+        }
+      }
+    }
+  }
+#endif
diff --git a/Sources/MistKit/CloudKitService/CloudKitError+ErrorDescription.swift b/Sources/MistKit/CloudKitService/CloudKitError+ErrorDescription.swift
index f8a92825..318a57a6 100644
--- a/Sources/MistKit/CloudKitService/CloudKitError+ErrorDescription.swift
+++ b/Sources/MistKit/CloudKitService/CloudKitError+ErrorDescription.swift
@@ -47,6 +47,8 @@ extension CloudKitError {
       return "CloudKit API error: HTTP \(statusCode)\nRaw Response: \(rawResponse)"
     case .invalidResponse:
       return "Invalid response from CloudKit"
+    case .incompleteResponse(let reason):
+      return Self.simpleReasonDescription(prefix: "Incomplete CloudKit response", reason: reason)
     case .conversionFailed(let conversionError):
       return "Failed to convert CloudKit response into a MistKit type: "
         + (conversionError.errorDescription ?? "\(conversionError)")
diff --git a/Sources/MistKit/CloudKitService/CloudKitError.swift b/Sources/MistKit/CloudKitService/CloudKitError.swift
index da508a5e..08928e81 100644
--- a/Sources/MistKit/CloudKitService/CloudKitError.swift
+++ b/Sources/MistKit/CloudKitService/CloudKitError.swift
@@ -52,6 +52,10 @@ public enum CloudKitError: LocalizedError, Sendable {
   /// rolled back because at least one operation in the batch failed.
   case atomicFailure(reason: String?)
   case invalidResponse
+  /// A multi-step convenience (e.g. `rereferenceAsset`) received a structurally
+  /// valid CloudKit response that lacked data it needed to proceed. `reason`
+  /// names exactly what was missing.
+  case incompleteResponse(reason: String)
   /// A CloudKit response decoded at the transport layer but a specific value
   /// could not be mapped into a MistKit domain type — e.g. an unmappable field
   /// value, a record/zone/user missing a required identifier, or an unknown
@@ -101,7 +105,7 @@ public enum CloudKitError: LocalizedError, Sendable {
       return 413
     case .badRequest, .atomicFailure:
       return 400
-    case .invalidResponse, .conversionFailed, .recordOperationFailed,
+    case .invalidResponse, .incompleteResponse, .conversionFailed, .recordOperationFailed,
       .subscriptionOperationFailed, .subscriptionLikelyDuplicate,
       .underlyingError, .decodingError, .networkError,
       .unsupportedOperationType, .paginationLimitExceeded,
diff --git a/Sources/MistKit/CloudKitService/CloudKitResponseProcessor+Changes.swift b/Sources/MistKit/CloudKitService/CloudKitResponseProcessor+Changes.swift
index 8a7bd2e6..c9cfb9d2 100644
--- a/Sources/MistKit/CloudKitService/CloudKitResponseProcessor+Changes.swift
+++ b/Sources/MistKit/CloudKitService/CloudKitResponseProcessor+Changes.swift
@@ -115,6 +115,21 @@ extension CloudKitResponseProcessor {
     }
   }
 
+  /// Process rereferenceAssets response
+  internal func processRereferenceAssetsResponse(
+    _ response: Operations.rereferenceAssets.Output
+  ) async throws(CloudKitError) -> Components.Schemas.AssetRereferenceResponse {
+    switch response {
+    case .ok(let okResponse):
+      switch okResponse.body {
+      case .json(let rereferenceData):
+        return rereferenceData
+      }
+    case .badRequest, .unauthorized, .undocumented:
+      throw CloudKitError(response) ?? .invalidResponse
+    }
+  }
+
   /// Process fetchZoneChanges response
   internal func processFetchZoneChangesResponse(_ response: Operations.fetchZoneChanges.Output)
     async throws(CloudKitError) -> Components.Schemas.ZoneChangesResponse
diff --git a/Sources/MistKit/CloudKitService/CloudKitService+AssetRereference.swift b/Sources/MistKit/CloudKitService/CloudKitService+AssetRereference.swift
new file mode 100644
index 00000000..e5fd83f2
--- /dev/null
+++ b/Sources/MistKit/CloudKitService/CloudKitService+AssetRereference.swift
@@ -0,0 +1,202 @@
+//
+//  CloudKitService+AssetRereference.swift
+//  MistKit
+//
+//  Created by Leo Dion.
+//  Copyright © 2026 BrightDigit.
+//
+//  Permission is hereby granted, free of charge, to any person
+//  obtaining a copy of this software and associated documentation
+//  files (the "Software"), to deal in the Software without
+//  restriction, including without limitation the rights to use,
+//  copy, modify, merge, publish, distribute, sublicense, and/or
+//  sell copies of the Software, and to permit persons to whom the
+//  Software is furnished to do so, subject to the following
+//  conditions:
+//
+//  The above copyright notice and this permission notice shall be
+//  included in all copies or substantial portions of the Software.
+//
+//  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+//  EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+//  OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+//  NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+//  HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+//  WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+//  FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+//  OTHER DEALINGS IN THE SOFTWARE.
+//
+
+internal import MistKitOpenAPI
+
+extension CloudKitService {
+  /// Fetch reusable asset descriptors for assets that already live on other
+  /// records, without re-uploading the bytes (`assets/rereference`).
+  ///
+  /// Each returned ``Asset`` descriptor can then be set on another record's
+  /// Asset field via `modifyRecords`/`updateRecord` to share the same
+  /// underlying asset. The asset's bytes are only deleted once all references
+  /// to it are removed.
+  ///
+  /// - Parameters:
+  ///   - fields: The source `(recordName, fieldName)` pairs to re-reference.
+  ///   - zoneID: Optional zone ID; defaults to the default zone when `nil`.
+  ///   - database: The CloudKit database scope (`.public`, `.private`, `.shared`).
+  /// - Returns: One reusable ``Asset`` descriptor per requested field, in order.
+  /// - Throws: ``CloudKitError``. The endpoint validates atomically — a bad
+  ///   entry (e.g. a missing source record) fails the *whole* request with a
+  ///   top-level ``CloudKitError/badRequest(reason:)``, so there are no
+  ///   per-item failures to inspect.
+  public func rereferenceAssets(
+    _ fields: [(recordName: String, fieldName: String)],
+    zoneID: ZoneID? = nil,
+    database: Database
+  ) async throws(CloudKitError) -> [Asset] {
+    do {
+      let assetRequests = fields.map { field in
+        Operations.rereferenceAssets.Input.Body.jsonPayload.assetsPayloadPayload(
+          recordName: field.recordName,
+          fieldName: field.fieldName
+        )
+      }
+
+      let requestBody = Operations.rereferenceAssets.Input.Body.jsonPayload(
+        zoneID: zoneID.map { Components.Schemas.ZoneID(from: $0) },
+        assets: assetRequests
+      )
+
+      let client = try self.client(for: database)
+      let response = try await client.rereferenceAssets(
+        path: Operations.rereferenceAssets.Input.Path(
+          containerIdentifier: containerIdentifier,
+          environment: environment,
+          database: database
+        ),
+        body: .json(requestBody)
+      )
+
+      let rereferenceData: Components.Schemas.AssetRereferenceResponse =
+        try await responseProcessor.processRereferenceAssetsResponse(response)
+
+      return (rereferenceData.assets ?? []).map { Asset(from: $0) }
+    } catch {
+      throw mapToCloudKitError(error, context: "rereferenceAssets")
+    }
+  }
+
+  /// Re-reference an asset from one record onto another in a single call.
+  ///
+  /// Composes `assets/rereference` with `records/modify`: fetches the reusable
+  /// descriptor for the source field, then writes it onto the target record's
+  /// asset field — sharing the same underlying asset bytes without re-uploading.
+  /// Mirrors the native `CloudKitStore.rereferenceAsset` convenience.
+  ///
+  /// The target record's `recordType` and current change tag are discovered via
+  /// a `lookupRecords` call, so callers need only name the target record. Callers
+  /// that already hold the target's `recordType` and `recordChangeTag` can skip
+  /// that round trip with the overload below.
+  ///
+  /// - Parameters:
+  ///   - sourceRecordName: The record holding the source asset.
+  ///   - assetField: The Asset field on the source record.
+  ///   - targetRecordName: The record that should reference the same asset.
+  ///   - targetField: The Asset field on the target record. Defaults to
+  ///     `assetField` when `nil`.
+  ///   - zoneID: Optional zone ID; defaults to the default zone when `nil`.
+  ///   - database: The CloudKit database scope (`.public`, `.private`, `.shared`).
+  /// - Returns: The updated target ``RecordInfo``.
+  /// - Throws: ``CloudKitError`` — a top-level failure (e.g.
+  ///   ``CloudKitError/badRequest(reason:)``) if the source asset could not be
+  ///   re-referenced; ``CloudKitError/incompleteResponse(reason:)`` if the target
+  ///   record is not found or carries no `recordType`; or a record failure if the
+  ///   target lookup or update failed.
+  public func rereferenceAsset(
+    fromRecord sourceRecordName: String,
+    field assetField: String,
+    toRecord targetRecordName: String,
+    field targetField: String? = nil,
+    zoneID: ZoneID? = nil,
+    database: Database
+  ) async throws(CloudKitError) -> RecordInfo {
+    let lookups = try await lookupRecords(
+      recordNames: [targetRecordName],
+      database: database
+    )
+    guard let firstLookup = lookups.first else {
+      throw CloudKitError.incompleteResponse(
+        reason: "target record '\(targetRecordName)' was not found"
+      )
+    }
+    let targetInfo = try firstLookup.get()
+    guard let recordType = targetInfo.recordType else {
+      throw CloudKitError.incompleteResponse(
+        reason: "target record '\(targetRecordName)' returned no recordType"
+      )
+    }
+
+    return try await rereferenceAsset(
+      fromRecord: sourceRecordName,
+      field: assetField,
+      toRecord: targetRecordName,
+      recordType: recordType,
+      recordChangeTag: targetInfo.recordChangeTag,
+      field: targetField,
+      zoneID: zoneID,
+      database: database
+    )
+  }
+
+  /// Re-reference an asset onto a target whose `recordType` and change tag are
+  /// already known, skipping the `lookupRecords` round trip the other overload
+  /// performs.
+  ///
+  /// - Parameters:
+  ///   - sourceRecordName: The record holding the source asset.
+  ///   - assetField: The Asset field on the source record.
+  ///   - targetRecordName: The record that should reference the same asset.
+  ///   - recordType: The target record's type.
+  ///   - recordChangeTag: The target record's current change tag, or `nil` to
+  ///     write without optimistic-concurrency checking.
+  ///   - targetField: The Asset field on the target record. Defaults to
+  ///     `assetField` when `nil`.
+  ///   - zoneID: Optional zone ID; defaults to the default zone when `nil`.
+  ///   - database: The CloudKit database scope (`.public`, `.private`, `.shared`).
+  /// - Returns: The updated target ``RecordInfo``.
+  /// - Throws: ``CloudKitError`` — a top-level failure (e.g.
+  ///   ``CloudKitError/badRequest(reason:)``) if the source asset could not be
+  ///   re-referenced; ``CloudKitError/incompleteResponse(reason:)`` if
+  ///   `assets/rereference` returned no descriptor for the source field; or a
+  ///   record failure if the target update failed.
+  public func rereferenceAsset(
+    fromRecord sourceRecordName: String,
+    field assetField: String,
+    toRecord targetRecordName: String,
+    recordType: String,
+    recordChangeTag: String?,
+    field targetField: String? = nil,
+    zoneID: ZoneID? = nil,
+    database: Database
+  ) async throws(CloudKitError) -> RecordInfo {
+    let resolvedTargetField = targetField ?? assetField
+
+    let descriptors = try await rereferenceAssets(
+      [(recordName: sourceRecordName, fieldName: assetField)],
+      zoneID: zoneID,
+      database: database
+    )
+    guard let asset = descriptors.first else {
+      throw CloudKitError.incompleteResponse(
+        reason: "assets/rereference returned no descriptor for record "
+          + "'\(sourceRecordName)' field '\(assetField)'"
+      )
+    }
+
+    return try await updateRecord(
+      recordType: recordType,
+      recordName: targetRecordName,
+      fields: [resolvedTargetField: .asset(asset)],
+      recordChangeTag: recordChangeTag,
+      database: database
+    )
+  }
+}
diff --git a/Sources/MistKit/Documentation.docc/HandlingErrors.md b/Sources/MistKit/Documentation.docc/HandlingErrors.md
index 9b6ddc22..d1942f2d 100644
--- a/Sources/MistKit/Documentation.docc/HandlingErrors.md
+++ b/Sources/MistKit/Documentation.docc/HandlingErrors.md
@@ -81,6 +81,7 @@ Every operation on ``CloudKitService`` throws ``CloudKitError``. The cases group
 | ``CloudKitError/httpErrorWithDetails(statusCode:serverErrorCode:reason:)`` | Depends on `serverErrorCode` | CloudKit returned a structured error |
 | ``CloudKitError/httpErrorWithRawResponse(statusCode:rawResponse:)`` | Sometimes | Validation rejection or unparseable error body |
 | ``CloudKitError/invalidResponse`` | No | Server returned 2xx but no payload |
+| ``CloudKitError/incompleteResponse(reason:)`` | No | A composed convenience got a valid response missing data it needed |
 | ``CloudKitError/networkError(_:)`` | Often | URLSession-level failure (timeout, DNS, TLS) |
 | ``CloudKitError/decodingError(_:)`` | No | Schema mismatch between OpenAPI client and CloudKit |
 | ``CloudKitError/underlyingError(_:)`` | Depends | Unclassified throw from the transport stack |
diff --git a/Sources/MistKit/Models/FieldValues/Asset+Components.swift b/Sources/MistKit/Models/FieldValues/Asset+Components.swift
new file mode 100644
index 00000000..1c82f08f
--- /dev/null
+++ b/Sources/MistKit/Models/FieldValues/Asset+Components.swift
@@ -0,0 +1,47 @@
+//
+//  Asset+Components.swift
+//  MistKit
+//
+//  Created by Leo Dion.
+//  Copyright © 2026 BrightDigit.
+//
+//  Permission is hereby granted, free of charge, to any person
+//  obtaining a copy of this software and associated documentation
+//  files (the "Software"), to deal in the Software without
+//  restriction, including without limitation the rights to use,
+//  copy, modify, merge, publish, distribute, sublicense, and/or
+//  sell copies of the Software, and to permit persons to whom the
+//  Software is furnished to do so, subject to the following
+//  conditions:
+//
+//  The above copyright notice and this permission notice shall be
+//  included in all copies or substantial portions of the Software.
+//
+//  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+//  EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+//  OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+//  NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+//  HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+//  WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+//  FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+//  OTHER DEALINGS IN THE SOFTWARE.
+//
+
+internal import MistKitOpenAPI
+
+extension Asset {
+  /// Initialize a domain ``Asset`` from a generated CloudKit asset dictionary.
+  ///
+  /// Used by ``FieldValue`` response conversion and by `rereferenceAssets`,
+  /// which receives reusable asset descriptors as bare `AssetValue` entries.
+  internal init(from assetValue: Components.Schemas.AssetValue) {
+    self.init(
+      fileChecksum: assetValue.fileChecksum,
+      size: assetValue.size,
+      referenceChecksum: assetValue.referenceChecksum,
+      wrappingKey: assetValue.wrappingKey,
+      receipt: assetValue.receipt,
+      downloadURL: assetValue.downloadURL
+    )
+  }
+}
diff --git a/Sources/MistKit/Models/FieldValues/FieldValue+Components.swift b/Sources/MistKit/Models/FieldValues/FieldValue+Components.swift
index c8cbf355..adaa5d6c 100644
--- a/Sources/MistKit/Models/FieldValues/FieldValue+Components.swift
+++ b/Sources/MistKit/Models/FieldValues/FieldValue+Components.swift
@@ -110,15 +110,7 @@ extension FieldValue {
 
   /// Initialize from asset field value
   internal init(assetValue: Components.Schemas.AssetValue) {
-    let asset = Asset(
-      fileChecksum: assetValue.fileChecksum,
-      size: assetValue.size,
-      referenceChecksum: assetValue.referenceChecksum,
-      wrappingKey: assetValue.wrappingKey,
-      receipt: assetValue.receipt,
-      downloadURL: assetValue.downloadURL
-    )
-    self = .asset(asset)
+    self = .asset(Asset(from: assetValue))
   }
 
   private static func makeComplexFieldValue(
diff --git a/Sources/MistKit/OpenAPI/OperationInputPath.swift b/Sources/MistKit/OpenAPI/OperationInputPath.swift
index 88a9fe3f..2de8f39a 100644
--- a/Sources/MistKit/OpenAPI/OperationInputPath.swift
+++ b/Sources/MistKit/OpenAPI/OperationInputPath.swift
@@ -87,6 +87,8 @@ extension Operations.queryRecords.Input.Path: OperationInputPath {}
 
 extension Operations.uploadAssets.Input.Path: OperationInputPath {}
 
+extension Operations.rereferenceAssets.Input.Path: OperationInputPath {}
+
 extension Operations.listSubscriptions.Input.Path: OperationInputPath {}
 
 extension Operations.lookupSubscriptions.Input.Path: OperationInputPath {}
diff --git a/Sources/MistKit/OpenAPI/Operations/Operations.rereferenceAssets.Output.swift b/Sources/MistKit/OpenAPI/Operations/Operations.rereferenceAssets.Output.swift
new file mode 100644
index 00000000..e5738706
--- /dev/null
+++ b/Sources/MistKit/OpenAPI/Operations/Operations.rereferenceAssets.Output.swift
@@ -0,0 +1,42 @@
+//
+//  Operations.rereferenceAssets.Output.swift
+//  MistKit
+//
+//  Created by Leo Dion.
+//  Copyright © 2026 BrightDigit.
+//
+//  Permission is hereby granted, free of charge, to any person
+//  obtaining a copy of this software and associated documentation
+//  files (the "Software"), to deal in the Software without
+//  restriction, including without limitation the rights to use,
+//  copy, modify, merge, publish, distribute, sublicense, and/or
+//  sell copies of the Software, and to permit persons to whom the
+//  Software is furnished to do so, subject to the following
+//  conditions:
+//
+//  The above copyright notice and this permission notice shall be
+//  included in all copies or substantial portions of the Software.
+//
+//  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+//  EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+//  OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+//  NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+//  HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+//  WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+//  FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+//  OTHER DEALINGS IN THE SOFTWARE.
+//
+
+internal import MistKitOpenAPI
+
+extension Operations.rereferenceAssets.Output: CloudKitResponseType {
+  internal func toCloudKitError() -> CloudKitError? {
+    switch self {
+    case .ok: return nil
+    case .badRequest(let response): return .init(response, statusCode: 400)
+    case .unauthorized(let response): return .init(response, statusCode: 401)
+    case .undocumented(let statusCode, _):
+      return .undocumented(statusCode: statusCode, response: self)
+    }
+  }
+}
diff --git a/Sources/MistKitOpenAPI/Client.swift b/Sources/MistKitOpenAPI/Client.swift
index f308f80a..114b8900 100644
--- a/Sources/MistKitOpenAPI/Client.swift
+++ b/Sources/MistKitOpenAPI/Client.swift
@@ -3387,6 +3387,134 @@ public struct Client: APIProtocol {
             }
         )
     }
+    /// Re-reference Existing Assets
+    ///
+    /// Fetch reusable asset descriptors for assets that already live on other
+    /// records, without re-uploading the bytes. Each returned descriptor can be
+    /// set on another record's Asset field via `records/modify` to share the
+    /// same underlying asset. Assets are deleted only when all references to
+    /// them are removed.
+    ///
+    /// Documented in Apple's archived CloudKit Web Services Reference
+    /// (`RereferenceAssets`); absent from the current online docs.
+    ///
+    ///
+    /// - Remark: HTTP `POST /database/{version}/{container}/{environment}/{database}/assets/rereference`.
+    /// - Remark: Generated from `#/paths//database/{version}/{container}/{environment}/{database}/assets/rereference/post(rereferenceAssets)`.
+    public func rereferenceAssets(_ input: Operations.rereferenceAssets.Input) async throws -> Operations.rereferenceAssets.Output {
+        try await client.send(
+            input: input,
+            forOperation: Operations.rereferenceAssets.id,
+            serializer: { input in
+                let path = try converter.renderedPath(
+                    template: "/database/{}/{}/{}/{}/assets/rereference",
+                    parameters: [
+                        input.path.version,
+                        input.path.container,
+                        input.path.environment,
+                        input.path.database
+                    ]
+                )
+                var request: HTTPTypes.HTTPRequest = .init(
+                    soar_path: path,
+                    method: .post
+                )
+                suppressMutabilityWarning(&request)
+                converter.setAcceptHeader(
+                    in: &request.headerFields,
+                    contentTypes: input.headers.accept
+                )
+                let body: OpenAPIRuntime.HTTPBody?
+                switch input.body {
+                case let .json(value):
+                    body = try converter.setRequiredRequestBodyAsJSON(
+                        value,
+                        headerFields: &request.headerFields,
+                        contentType: "application/json; charset=utf-8"
+                    )
+                }
+                return (request, body)
+            },
+            deserializer: { response, responseBody in
+                switch response.status.code {
+                case 200:
+                    let contentType = converter.extractContentTypeIfPresent(in: response.headerFields)
+                    let body: Operations.rereferenceAssets.Output.Ok.Body
+                    let chosenContentType = try converter.bestContentType(
+                        received: contentType,
+                        options: [
+                            "application/json"
+                        ]
+                    )
+                    switch chosenContentType {
+                    case "application/json":
+                        body = try await converter.getResponseBodyAsJSON(
+                            Components.Schemas.AssetRereferenceResponse.self,
+                            from: responseBody,
+                            transforming: { value in
+                                .json(value)
+                            }
+                        )
+                    default:
+                        preconditionFailure("bestContentType chose an invalid content type.")
+                    }
+                    return .ok(.init(body: body))
+                case 400:
+                    let contentType = converter.extractContentTypeIfPresent(in: response.headerFields)
+                    let body: Components.Responses.Failure.Body
+                    let chosenContentType = try converter.bestContentType(
+                        received: contentType,
+                        options: [
+                            "application/json"
+                        ]
+                    )
+                    switch chosenContentType {
+                    case "application/json":
+                        body = try await converter.getResponseBodyAsJSON(
+                            Components.Schemas.ErrorResponse.self,
+                            from: responseBody,
+                            transforming: { value in
+                                .json(value)
+                            }
+                        )
+                    default:
+                        preconditionFailure("bestContentType chose an invalid content type.")
+                    }
+                    return .badRequest(.init(body: body))
+                case 401:
+                    let contentType = converter.extractContentTypeIfPresent(in: response.headerFields)
+                    let body: Components.Responses.Failure.Body
+                    let chosenContentType = try converter.bestContentType(
+                        received: contentType,
+                        options: [
+                            "application/json"
+                        ]
+                    )
+                    switch chosenContentType {
+                    case "application/json":
+                        body = try await converter.getResponseBodyAsJSON(
+                            Components.Schemas.ErrorResponse.self,
+                            from: responseBody,
+                            transforming: { value in
+                                .json(value)
+                            }
+                        )
+                    default:
+                        preconditionFailure("bestContentType chose an invalid content type.")
+                    }
+                    return .unauthorized(.init(body: body))
+                default:
+                    return .undocumented(
+                        statusCode: response.status.code,
+                        .init(
+                            headerFields: response.headerFields,
+                            body: responseBody
+                        )
+                    )
+                }
+            }
+        )
+    }
     /// Create APNs Token
     ///
     /// Create an Apple Push Notification service (APNs) token.
diff --git a/Sources/MistKitOpenAPI/Types.swift b/Sources/MistKitOpenAPI/Types.swift
index 1d7fe758..9d1ed41b 100644
--- a/Sources/MistKitOpenAPI/Types.swift
+++ b/Sources/MistKitOpenAPI/Types.swift
@@ -158,6 +158,21 @@ public protocol APIProtocol: Sendable {
     /// - Remark: HTTP `POST /database/{version}/{container}/{environment}/{database}/assets/upload`.
     /// - Remark: Generated from `#/paths//database/{version}/{container}/{environment}/{database}/assets/upload/post(uploadAssets)`.
     func uploadAssets(_ input: Operations.uploadAssets.Input) async throws -> Operations.uploadAssets.Output
+    /// Re-reference Existing Assets
+    ///
+    /// Fetch reusable asset descriptors for assets that already live on other
+    /// records, without re-uploading the bytes. Each returned descriptor can be
+    /// set on another record's Asset field via `records/modify` to share the
+    /// same underlying asset. Assets are deleted only when all references to
+    /// them are removed.
+    ///
+    /// Documented in Apple's archived CloudKit Web Services Reference
+    /// (`RereferenceAssets`); absent from the current online docs.
+    ///
+    ///
+    /// - Remark: HTTP `POST /database/{version}/{container}/{environment}/{database}/assets/rereference`.
+    /// - Remark: Generated from `#/paths//database/{version}/{container}/{environment}/{database}/assets/rereference/post(rereferenceAssets)`.
+    func rereferenceAssets(_ input: Operations.rereferenceAssets.Input) async throws -> Operations.rereferenceAssets.Output
     /// Create APNs Token
     ///
     /// Create an Apple Push Notification service (APNs) token.
@@ -504,6 +519,31 @@ extension APIProtocol {
             body: body
         ))
     }
+    /// Re-reference Existing Assets
+    ///
+    /// Fetch reusable asset descriptors for assets that already live on other
+    /// records, without re-uploading the bytes. Each returned descriptor can be
+    /// set on another record's Asset field via `records/modify` to share the
+    /// same underlying asset. Assets are deleted only when all references to
+    /// them are removed.
+    ///
+    /// Documented in Apple's archived CloudKit Web Services Reference
+    /// (`RereferenceAssets`); absent from the current online docs.
+    ///
+    ///
+    /// - Remark: HTTP `POST /database/{version}/{container}/{environment}/{database}/assets/rereference`.
+    /// - Remark: Generated from `#/paths//database/{version}/{container}/{environment}/{database}/assets/rereference/post(rereferenceAssets)`.
+    public func rereferenceAssets(
+        path: Operations.rereferenceAssets.Input.Path,
+        headers: Operations.rereferenceAssets.Input.Headers = .init(),
+        body: Operations.rereferenceAssets.Input.Body
+    ) async throws -> Operations.rereferenceAssets.Output {
+        try await rereferenceAssets(Operations.rereferenceAssets.Input(
+            path: path,
+            headers: headers,
+            body: body
+        ))
+    }
     /// Create APNs Token
     ///
     /// Create an Apple Push Notification service (APNs) token.
@@ -2474,6 +2514,30 @@ public enum Components {
                 case tokens
             }
         }
+        /// Response body for `assets/rereference`: one reusable asset descriptor
+        /// per requested asset field, wrapped under `assets`.
+        ///
+        /// Verified against the live service: the endpoint validates atomically —
+        /// a bad entry (e.g. a missing source record) fails the *whole* request
+        /// with a top-level HTTP 400, so there are no inline per-item failures
+        /// here (unlike `ModifyResponse`/`LookupResponse`).
+        ///
+        ///
+        /// - Remark: Generated from `#/components/schemas/AssetRereferenceResponse`.
+        public struct AssetRereferenceResponse: Codable, Hashable, Sendable {
+            /// - Remark: Generated from `#/components/schemas/AssetRereferenceResponse/assets`.
+            public var assets: [Components.Schemas.AssetValue]?
+            /// Creates a new `AssetRereferenceResponse`.
+            ///
+            /// - Parameters:
+            ///   - assets:
+            public init(assets: [Components.Schemas.AssetValue]? = nil) {
+                self.assets = assets
+            }
+            public enum CodingKeys: String, CodingKey {
+                case assets
+            }
+        }
         /// Response body for `tokens/create`. Per Apple's archived REST reference,
         /// the server returns the echoed environment, the minted APNs token, and a
         /// long-poll URL that browser/Service-Worker callers use to receive push
@@ -9702,6 +9766,317 @@ public enum Operations {
             }
         }
     }
+    /// Re-reference Existing Assets
+    ///
+    /// Fetch reusable asset descriptors for assets that already live on other
+    /// records, without re-uploading the bytes. Each returned descriptor can be
+    /// set on another record's Asset field via `records/modify` to share the
+    /// same underlying asset. Assets are deleted only when all references to
+    /// them are removed.
+    ///
+    /// Documented in Apple's archived CloudKit Web Services Reference
+    /// (`RereferenceAssets`); absent from the current online docs.
+    ///
+    ///
+    /// - Remark: HTTP `POST /database/{version}/{container}/{environment}/{database}/assets/rereference`.
+    /// - Remark: Generated from `#/paths//database/{version}/{container}/{environment}/{database}/assets/rereference/post(rereferenceAssets)`.
+    public enum rereferenceAssets {
+        public static let id: Swift.String = "rereferenceAssets"
+        public struct Input: Sendable, Hashable {
+            /// - Remark: Generated from `#/paths/database/{version}/{container}/{environment}/{database}/assets/rereference/POST/path`.
+            public struct Path: Sendable, Hashable {
+                /// - Remark: Generated from `#/paths/database/{version}/{container}/{environment}/{database}/assets/rereference/POST/path/version`.
+                public var version: Components.Parameters.version
+                /// - Remark: Generated from `#/paths/database/{version}/{container}/{environment}/{database}/assets/rereference/POST/path/container`.
+                public var container: Components.Parameters.container
+                /// Container environment
+                ///
+                /// - Remark: Generated from `#/components/parameters/environment`.
+                @frozen public enum environment: String, Codable, Hashable, Sendable, CaseIterable {
+                    case development = "development"
+                    case production = "production"
+                }
+                /// - Remark: Generated from `#/paths/database/{version}/{container}/{environment}/{database}/assets/rereference/POST/path/environment`.
+                public var environment: Components.Parameters.environment
+                /// Database scope
+                ///
+                /// - Remark: Generated from `#/components/parameters/database`.
+                @frozen public enum database: String, Codable, Hashable, Sendable, CaseIterable {
+                    case _public = "public"
+                    case _private = "private"
+                    case shared = "shared"
+                }
+                /// - Remark: Generated from `#/paths/database/{version}/{container}/{environment}/{database}/assets/rereference/POST/path/database`.
+                public var database: Components.Parameters.database
+                /// Creates a new `Path`.
+                ///
+                /// - Parameters:
+                ///   - version:
+                ///   - container:
+                ///   - environment:
+                ///   - database:
+                public init(
+                    version: Components.Parameters.version,
+                    container: Components.Parameters.container,
+                    environment: Components.Parameters.environment,
+                    database: Components.Parameters.database
+                ) {
+                    self.version = version
+                    self.container = container
+                    self.environment = environment
+                    self.database = database
+                }
+            }
+            public var path: Operations.rereferenceAssets.Input.Path
+            /// - Remark: Generated from `#/paths/database/{version}/{container}/{environment}/{database}/assets/rereference/POST/header`.
+            public struct Headers: Sendable, Hashable {
+                public var accept: [OpenAPIRuntime.AcceptHeaderContentType]
+                /// Creates a new `Headers`.
+                ///
+                /// - Parameters:
+                ///   - accept:
+                public init(accept: [OpenAPIRuntime.AcceptHeaderContentType] = .defaultValues()) {
+                    self.accept = accept
+                }
+            }
+            public var headers: Operations.rereferenceAssets.Input.Headers
+            /// - Remark: Generated from `#/paths/database/{version}/{container}/{environment}/{database}/assets/rereference/POST/requestBody`.
+            @frozen public enum Body: Sendable, Hashable {
+                /// - Remark: Generated from `#/paths/database/{version}/{container}/{environment}/{database}/assets/rereference/POST/requestBody/json`.
+                public struct jsonPayload: Codable, Hashable, Sendable {
+                    /// - Remark: Generated from `#/paths/database/{version}/{container}/{environment}/{database}/assets/rereference/POST/requestBody/json/zoneID`.
+                    public var zoneID: Components.Schemas.ZoneID?
+                    /// - Remark: Generated from `#/paths/database/{version}/{container}/{environment}/{database}/assets/rereference/POST/requestBody/json/assetsPayload`.
+                    public struct assetsPayloadPayload: Codable, Hashable, Sendable {
+                        /// Name of the record holding the source asset.
+                        ///
+                        /// - Remark: Generated from `#/paths/database/{version}/{container}/{environment}/{database}/assets/rereference/POST/requestBody/json/assetsPayload/recordName`.
+                        public var recordName: Swift.String
+                        /// Name of the Asset field on the source record.
+                        ///
+                        /// - Remark: Generated from `#/paths/database/{version}/{container}/{environment}/{database}/assets/rereference/POST/requestBody/json/assetsPayload/fieldName`.
+                        public var fieldName: Swift.String
+                        /// Creates a new `assetsPayloadPayload`.
+                        ///
+                        /// - Parameters:
+                        ///   - recordName: Name of the record holding the source asset.
+                        ///   - fieldName: Name of the Asset field on the source record.
+                        public init(
+                            recordName: Swift.String,
+                            fieldName: Swift.String
+                        ) {
+                            self.recordName = recordName
+                            self.fieldName = fieldName
+                        }
+                        public enum CodingKeys: String, CodingKey {
+                            case recordName
+                            case fieldName
+                        }
+                    }
+                    /// Array of source asset fields to re-reference.
+                    ///
+                    /// - Remark: Generated from `#/paths/database/{version}/{container}/{environment}/{database}/assets/rereference/POST/requestBody/json/assets`.
+                    public typealias assetsPayload = [Operations.rereferenceAssets.Input.Body.jsonPayload.assetsPayloadPayload]
+                    /// Array of source asset fields to re-reference.
+                    ///
+                    /// - Remark: Generated from `#/paths/database/{version}/{container}/{environment}/{database}/assets/rereference/POST/requestBody/json/assets`.
+                    public var assets: Operations.rereferenceAssets.Input.Body.jsonPayload.assetsPayload
+                    /// Creates a new `jsonPayload`.
+                    ///
+                    /// - Parameters:
+                    ///   - zoneID:
+                    ///   - assets: Array of source asset fields to re-reference.
+                    public init(
+                        zoneID: Components.Schemas.ZoneID? = nil,
+                        assets: Operations.rereferenceAssets.Input.Body.jsonPayload.assetsPayload
+                    ) {
+                        self.zoneID = zoneID
+                        self.assets = assets
+                    }
+                    public enum CodingKeys: String, CodingKey {
+                        case zoneID
+                        case assets
+                    }
+                }
+                /// - Remark: Generated from `#/paths/database/{version}/{container}/{environment}/{database}/assets/rereference/POST/requestBody/content/application\/json`.
+                case json(Operations.rereferenceAssets.Input.Body.jsonPayload)
+            }
+            public var body: Operations.rereferenceAssets.Input.Body
+            /// Creates a new `Input`.
+            ///
+            /// - Parameters:
+            ///   - path:
+            ///   - headers:
+            ///   - body:
+            public init(
+                path: Operations.rereferenceAssets.Input.Path,
+                headers: Operations.rereferenceAssets.Input.Headers = .init(),
+                body: Operations.rereferenceAssets.Input.Body
+            ) {
+                self.path = path
+                self.headers = headers
+                self.body = body
+            }
+        }
+        @frozen public enum Output: Sendable, Hashable {
+            public struct Ok: Sendable, Hashable {
+                /// - Remark: Generated from `#/paths/database/{version}/{container}/{environment}/{database}/assets/rereference/POST/responses/200/content`.
+                @frozen public enum Body: Sendable, Hashable {
+                    /// - Remark: Generated from `#/paths/database/{version}/{container}/{environment}/{database}/assets/rereference/POST/responses/200/content/application\/json`.
+                    case json(Components.Schemas.AssetRereferenceResponse)
+                    /// The associated value of the enum case if `self` is `.json`.
+                    ///
+                    /// - Throws: An error if `self` is not `.json`.
+                    /// - SeeAlso: `.json`.
+                    public var json: Components.Schemas.AssetRereferenceResponse {
+                        get throws {
+                            switch self {
+                            case let .json(body):
+                                return body
+                            }
+                        }
+                    }
+                }
+                /// Received HTTP response body
+                public var body: Operations.rereferenceAssets.Output.Ok.Body
+                /// Creates a new `Ok`.
+                ///
+                /// - Parameters:
+                ///   - body: Received HTTP response body
+                public init(body: Operations.rereferenceAssets.Output.Ok.Body) {
+                    self.body = body
+                }
+            }
+            /// Reusable asset descriptors returned successfully.
+            ///
+            /// - Remark: Generated from `#/paths//database/{version}/{container}/{environment}/{database}/assets/rereference/post(rereferenceAssets)/responses/200`.
+            ///
+            /// HTTP response code: `200 ok`.
+            case ok(Operations.rereferenceAssets.Output.Ok)
+            /// The associated value of the enum case if `self` is `.ok`.
+            ///
+            /// - Throws: An error if `self` is not `.ok`.
+            /// - SeeAlso: `.ok`.
+            public var ok: Operations.rereferenceAssets.Output.Ok {
+                get throws {
+                    switch self {
+                    case let .ok(response):
+                        return response
+                    default:
+                        try throwUnexpectedResponseStatus(
+                            expectedStatus: "ok",
+                            response: self
+                        )
+                    }
+                }
+            }
+            /// Error response shared by all endpoints. The body schema is the same for
+            /// every 4xx/5xx status code; the HTTP status code itself disambiguates
+            /// which CloudKit failure occurred. See Apple's CloudKit Web Services
+            /// Error Codes documentation for the full code → status mapping:
+            /// - 400 BadRequest (BAD_REQUEST, ATOMIC_ERROR)
+            /// - 401 Unauthorized (AUTHENTICATION_FAILED)
+            /// - 403 Forbidden (ACCESS_DENIED)
+            /// - 404 NotFound (NOT_FOUND, ZONE_NOT_FOUND)
+            /// - 409 Conflict (CONFLICT, EXISTS)
+            /// - 412 PreconditionFailed (VALIDATING_REFERENCE_ERROR)
+            /// - 413 RequestEntityTooLarge (QUOTA_EXCEEDED)
+            /// - 421 UnprocessableEntity (AUTHENTICATION_REQUIRED)
+            /// - 429 TooManyRequests (THROTTLED)
+            /// - 500 InternalServerError (INTERNAL_ERROR)
+            /// - 503 ServiceUnavailable (TRY_AGAIN_LATER)
+            ///
+            ///
+            /// - Remark: Generated from `#/paths//database/{version}/{container}/{environment}/{database}/assets/rereference/post(rereferenceAssets)/responses/400`.
+            ///
+            /// HTTP response code: `400 badRequest`.
+            case badRequest(Components.Responses.Failure)
+            /// The associated value of the enum case if `self` is `.badRequest`.
+            ///
+            /// - Throws: An error if `self` is not `.badRequest`.
+            /// - SeeAlso: `.badRequest`.
+            public var badRequest: Components.Responses.Failure {
+                get throws {
+                    switch self {
+                    case let .badRequest(response):
+                        return response
+                    default:
+                        try throwUnexpectedResponseStatus(
+                            expectedStatus: "badRequest",
+                            response: self
+                        )
+                    }
+                }
+            }
+            /// Error response shared by all endpoints. The body schema is the same for
+            /// every 4xx/5xx status code; the HTTP status code itself disambiguates
+            /// which CloudKit failure occurred. See Apple's CloudKit Web Services
+            /// Error Codes documentation for the full code → status mapping:
+            /// - 400 BadRequest (BAD_REQUEST, ATOMIC_ERROR)
+            /// - 401 Unauthorized (AUTHENTICATION_FAILED)
+            /// - 403 Forbidden (ACCESS_DENIED)
+            /// - 404 NotFound (NOT_FOUND, ZONE_NOT_FOUND)
+            /// - 409 Conflict (CONFLICT, EXISTS)
+            /// - 412 PreconditionFailed (VALIDATING_REFERENCE_ERROR)
+            /// - 413 RequestEntityTooLarge (QUOTA_EXCEEDED)
+            /// - 421 UnprocessableEntity (AUTHENTICATION_REQUIRED)
+            /// - 429 TooManyRequests (THROTTLED)
+            /// - 500 InternalServerError (INTERNAL_ERROR)
+            /// - 503 ServiceUnavailable (TRY_AGAIN_LATER)
+            ///
+            ///
+            /// - Remark: Generated from `#/paths//database/{version}/{container}/{environment}/{database}/assets/rereference/post(rereferenceAssets)/responses/401`.
+            ///
+            /// HTTP response code: `401 unauthorized`.
+            case unauthorized(Components.Responses.Failure)
+            /// The associated value of the enum case if `self` is `.unauthorized`.
+            ///
+            /// - Throws: An error if `self` is not `.unauthorized`.
+            /// - SeeAlso: `.unauthorized`.
+            public var unauthorized: Components.Responses.Failure {
+                get throws {
+                    switch self {
+                    case let .unauthorized(response):
+                        return response
+                    default:
+                        try throwUnexpectedResponseStatus(
+                            expectedStatus: "unauthorized",
+                            response: self
+                        )
+                    }
+                }
+            }
+            /// Undocumented response.
+            ///
+            /// A response with a code that is not documented in the OpenAPI document.
+            case undocumented(statusCode: Swift.Int, OpenAPIRuntime.UndocumentedPayload)
+        }
+        @frozen public enum AcceptableContentType: AcceptableProtocol {
+            case json
+            case other(Swift.String)
+            public init?(rawValue: Swift.String) {
+                switch rawValue.lowercased() {
+                case "application/json":
+                    self = .json
+                default:
+                    self = .other(rawValue)
+                }
+            }
+            public var rawValue: Swift.String {
+                switch self {
+                case let .other(string):
+                    return string
+                case .json:
+                    return "application/json"
+                }
+            }
+            public static var allCases: [Self] {
+                [
+                    .json
+                ]
+            }
+        }
+    }
     /// Create APNs Token
     ///
     /// Create an Apple Push Notification service (APNs) token.
diff --git a/Tests/MistKitTests/CloudKitService/Rereference/CloudKitServiceTests.Rereference+Compose.swift b/Tests/MistKitTests/CloudKitService/Rereference/CloudKitServiceTests.Rereference+Compose.swift
new file mode 100644
index 00000000..f793fe83
--- /dev/null
+++ b/Tests/MistKitTests/CloudKitService/Rereference/CloudKitServiceTests.Rereference+Compose.swift
@@ -0,0 +1,108 @@
+//
+//  CloudKitServiceTests.Rereference+Compose.swift
+//  MistKit
+//
+//  Created by Leo Dion.
+//  Copyright © 2026 BrightDigit.
+//
+//  Permission is hereby granted, free of charge, to any person
+//  obtaining a copy of this software and associated documentation
+//  files (the "Software"), to deal in the Software without
+//  restriction, including without limitation the rights to use,
+//  copy, modify, merge, publish, distribute, sublicense, and/or
+//  sell copies of the Software, and to permit persons to whom the
+//  Software is furnished to do so, subject to the following
+//  conditions:
+//
+//  The above copyright notice and this permission notice shall be
+//  included in all copies or substantial portions of the Software.
+//
+//  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+//  EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+//  OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+//  NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+//  HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+//  WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+//  FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+//  OTHER DEALINGS IN THE SOFTWARE.
+//
+
+internal import Foundation
+internal import Testing
+
+@testable import MistKit
+
+extension CloudKitServiceTests.Rereference {
+  @Suite("Rereference Asset Compose")
+  internal struct Compose {
+    private typealias Helper = CloudKitServiceTests.Rereference
+
+    @Test("rereferenceAsset reuses the descriptor and returns the updated target")
+    internal func reusesDescriptorOntoTarget() async throws {
+      guard #available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *) else {
+        Issue.record("CloudKitService is not available on this operating system.")
+        return
+      }
+
+      // rereference → reusable descriptor; lookup → target's type/changeTag;
+      // modify → the saved target now carrying the same asset checksum.
+      let service = try Helper.makeService(responsesByOperation: [
+        "rereferenceAssets": try Helper.rereferenceResponse(assets: [
+          Helper.assetDictionary(fileChecksum: "shared-chk")
+        ]),
+        "lookupRecords": try Helper.recordsResponse([
+          Helper.noteRecord(recordName: "note-b", changeTag: "tag-b")
+        ]),
+        "modifyRecords": try Helper.recordsResponse([
+          Helper.noteRecord(
+            recordName: "note-b", changeTag: "tag-b2", imageChecksum: "shared-chk"
+          )
+        ]),
+      ])
+
+      let updated = try await service.rereferenceAsset(
+        fromRecord: "note-a",
+        field: "image",
+        toRecord: "note-b",
+        database: Helper.publicDatabase
+      )
+
+      #expect(updated.recordName == "note-b")
+      guard case .asset(let asset) = updated.fields["image"] else {
+        Issue.record("Updated target should carry an image asset")
+        return
+      }
+      #expect(asset.fileChecksum == "shared-chk")
+    }
+
+    @Test("rereferenceAsset overload skips the lookupRecords round trip")
+    internal func overloadSkipsLookup() async throws {
+      guard #available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *) else {
+        Issue.record("CloudKitService is not available on this operating system.")
+        return
+      }
+
+      let (service, provider) = try Helper.makeServiceWithProvider(responsesByOperation: [
+        "rereferenceAssets": try Helper.rereferenceResponse(assets: [
+          Helper.assetDictionary(fileChecksum: "shared-chk")
+        ]),
+        "modifyRecords": try Helper.recordsResponse([
+          Helper.noteRecord(recordName: "note-b", changeTag: "tag-b2", imageChecksum: "shared-chk")
+        ]),
+      ])
+
+      let updated = try await service.rereferenceAsset(
+        fromRecord: "note-a",
+        field: "image",
+        toRecord: "note-b",
+        recordType: "Note",
+        recordChangeTag: "tag-b",
+        database: Helper.publicDatabase
+      )
+
+      #expect(updated.recordName == "note-b")
+      #expect(await provider.callCount(for: "lookupRecords") == 0)
+      #expect(await provider.callCount(for: "modifyRecords") == 1)
+    }
+  }
+}
diff --git a/Tests/MistKitTests/CloudKitService/Rereference/CloudKitServiceTests.Rereference+ComposeErrors.swift b/Tests/MistKitTests/CloudKitService/Rereference/CloudKitServiceTests.Rereference+ComposeErrors.swift
new file mode 100644
index 00000000..2ac668eb
--- /dev/null
+++ b/Tests/MistKitTests/CloudKitService/Rereference/CloudKitServiceTests.Rereference+ComposeErrors.swift
@@ -0,0 +1,171 @@
+//
+//  CloudKitServiceTests.Rereference+ComposeErrors.swift
+//  MistKit
+//
+//  Created by Leo Dion.
+//  Copyright © 2026 BrightDigit.
+//
+//  Permission is hereby granted, free of charge, to any person
+//  obtaining a copy of this software and associated documentation
+//  files (the "Software"), to deal in the Software without
+//  restriction, including without limitation the rights to use,
+//  copy, modify, merge, publish, distribute, sublicense, and/or
+//  sell copies of the Software, and to permit persons to whom the
+//  Software is furnished to do so, subject to the following
+//  conditions:
+//
+//  The above copyright notice and this permission notice shall be
+//  included in all copies or substantial portions of the Software.
+//
+//  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+//  EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+//  OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+//  NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+//  HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+//  WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+//  FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+//  OTHER DEALINGS IN THE SOFTWARE.
+//
+
+internal import Foundation
+internal import Testing
+
+@testable import MistKit
+
+extension CloudKitServiceTests.Rereference {
+  @Suite("Rereference Asset Compose Errors")
+  internal struct ComposeErrors {
+    private typealias Helper = CloudKitServiceTests.Rereference
+
+    /// Field keys written by the first operation in a `records/modify` body.
+    private static func modifyFieldKeys(from body: Data) throws -> Set {
+      let json = try JSONSerialization.jsonObject(with: body)
+      let object = try #require(json as? [String: Any])
+      let operations = try #require(object["operations"] as? [[String: Any]])
+      let record = try #require(operations.first?["record"] as? [String: Any])
+      let fields = try #require(record["fields"] as? [String: Any])
+      return Set(fields.keys)
+    }
+
+    @Test("rereferenceAsset throws .incompleteResponse when no source descriptor is returned")
+    internal func throwsWhenNoSourceDescriptor() async throws {
+      guard #available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *) else {
+        Issue.record("CloudKitService is not available on this operating system.")
+        return
+      }
+      // The overload supplies recordType/changeTag, so the only round trip is
+      // assets/rereference — here it returns an empty descriptor list.
+      let service = try Helper.makeService(responsesByOperation: [
+        "rereferenceAssets": try Helper.rereferenceResponse(assets: [])
+      ])
+
+      let error = await #expect(throws: CloudKitError.self) {
+        _ = try await service.rereferenceAsset(
+          fromRecord: "note-a",
+          field: "image",
+          toRecord: "note-b",
+          recordType: "Note",
+          recordChangeTag: "tag-b",
+          database: Helper.publicDatabase
+        )
+      }
+      guard case .incompleteResponse(let reason) = error else {
+        Issue.record("Expected .incompleteResponse, got \(String(describing: error))")
+        return
+      }
+      #expect(reason.contains("note-a"))
+      #expect(reason.contains("image"))
+    }
+
+    @Test("rereferenceAsset throws .incompleteResponse when the target record is not found")
+    internal func throwsWhenTargetNotFound() async throws {
+      guard #available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *) else {
+        Issue.record("CloudKitService is not available on this operating system.")
+        return
+      }
+      let service = try Helper.makeService(responsesByOperation: [
+        "rereferenceAssets": try Helper.rereferenceResponse(assets: [
+          Helper.assetDictionary(fileChecksum: "shared-chk")
+        ]),
+        "lookupRecords": try Helper.recordsResponse([]),
+      ])
+
+      let error = await #expect(throws: CloudKitError.self) {
+        _ = try await service.rereferenceAsset(
+          fromRecord: "note-a",
+          field: "image",
+          toRecord: "missing-target",
+          database: Helper.publicDatabase
+        )
+      }
+      guard case .incompleteResponse(let reason) = error else {
+        Issue.record("Expected .incompleteResponse, got \(String(describing: error))")
+        return
+      }
+      #expect(reason.contains("missing-target"))
+      #expect(reason.contains("not found"))
+    }
+
+    @Test("rereferenceAsset throws .incompleteResponse when the target has no recordType")
+    internal func throwsWhenTargetHasNoRecordType() async throws {
+      guard #available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *) else {
+        Issue.record("CloudKitService is not available on this operating system.")
+        return
+      }
+      let service = try Helper.makeService(responsesByOperation: [
+        "rereferenceAssets": try Helper.rereferenceResponse(assets: [
+          Helper.assetDictionary(fileChecksum: "shared-chk")
+        ]),
+        "lookupRecords": try Helper.recordsResponse([
+          Helper.recordWithoutType(recordName: "note-b")
+        ]),
+      ])
+
+      let error = await #expect(throws: CloudKitError.self) {
+        _ = try await service.rereferenceAsset(
+          fromRecord: "note-a",
+          field: "image",
+          toRecord: "note-b",
+          database: Helper.publicDatabase
+        )
+      }
+      guard case .incompleteResponse(let reason) = error else {
+        Issue.record("Expected .incompleteResponse, got \(String(describing: error))")
+        return
+      }
+      #expect(reason.contains("recordType"))
+    }
+
+    @Test("rereferenceAsset writes onto assetField when targetField is omitted")
+    internal func defaultsTargetFieldToAssetField() async throws {
+      guard #available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *) else {
+        Issue.record("CloudKitService is not available on this operating system.")
+        return
+      }
+      let (service, provider) = try Helper.makeServiceWithProvider(responsesByOperation: [
+        "rereferenceAssets": try Helper.rereferenceResponse(assets: [
+          Helper.assetDictionary(fileChecksum: "shared-chk")
+        ]),
+        "lookupRecords": try Helper.recordsResponse([
+          Helper.noteRecord(recordName: "note-b", changeTag: "tag-b")
+        ]),
+        "modifyRecords": try Helper.recordsResponse([
+          Helper.noteRecord(recordName: "note-b", changeTag: "tag-b2", imageChecksum: "shared-chk")
+        ]),
+      ])
+
+      // No `field:` argument — the target field must default to the source field.
+      _ = try await service.rereferenceAsset(
+        fromRecord: "note-a",
+        field: "photo",
+        toRecord: "note-b",
+        database: Helper.publicDatabase
+      )
+
+      let modifyBodies = await provider.bodies(for: "modifyRecords")
+      let body = try #require(modifyBodies.first.flatMap { $0 })
+      let fieldKeys = try Self.modifyFieldKeys(from: body)
+      #expect(fieldKeys.contains("photo"))
+    }
+  }
+}
diff --git a/Tests/MistKitTests/CloudKitService/Rereference/CloudKitServiceTests.Rereference+Helpers.swift b/Tests/MistKitTests/CloudKitService/Rereference/CloudKitServiceTests.Rereference+Helpers.swift
new file mode 100644
index 00000000..aa1c3f58
--- /dev/null
+++ b/Tests/MistKitTests/CloudKitService/Rereference/CloudKitServiceTests.Rereference+Helpers.swift
@@ -0,0 +1,138 @@
+//
+//  CloudKitServiceTests.Rereference+Helpers.swift
+//  MistKit
+//
+//  Created by Leo Dion.
+//  Copyright © 2026 BrightDigit.
+//
+//  Permission is hereby granted, free of charge, to any person
+//  obtaining a copy of this software and associated documentation
+//  files (the "Software"), to deal in the Software without
+//  restriction, including without limitation the rights to use,
+//  copy, modify, merge, publish, distribute, sublicense, and/or
+//  sell copies of the Software, and to permit persons to whom the
+//  Software is furnished to do so, subject to the following
+//  conditions:
+//
+//  The above copyright notice and this permission notice shall be
+//  included in all copies or substantial portions of the Software.
+//
+//  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+//  EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+//  OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+//  NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+//  HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+//  WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+//  FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+//  OTHER DEALINGS IN THE SOFTWARE.
+//
+
+internal import Foundation
+internal import HTTPTypes
+internal import Testing
+
+@testable import MistKit
+
+extension CloudKitServiceTests {
+  internal enum Rereference {}
+}
+
+extension CloudKitServiceTests.Rereference {
+  internal static let publicDatabase: Database = .public(.prefers(.serverToServer))
+
+  /// Build a service whose mock transport answers each operation by ID. Any
+  /// operation without an explicit response falls back to an empty success.
+  internal static func makeService(
+    responsesByOperation: [String: ResponseConfig]
+  ) throws -> CloudKitService {
+    try makeServiceWithProvider(responsesByOperation: responsesByOperation).service
+  }
+
+  /// Like ``makeService(responsesByOperation:)`` but also returns the
+  /// `ResponseProvider` so tests can inspect recorded request bodies / call counts.
+  internal static func makeServiceWithProvider(
+    responsesByOperation: [String: ResponseConfig]
+  ) throws -> (service: CloudKitService, provider: ResponseProvider) {
+    let provider = ResponseProvider(
+      responses: responsesByOperation,
+      defaultResponse: .success(body: Data("{}".utf8))
+    )
+    let transport = MockTransport(responseProvider: provider)
+    let service = try CloudKitService(
+      containerIdentifier: TestConstants.serviceContainerIdentifier,
+      credentials: Credentials(apiAuth: APICredentials(apiToken: TestConstants.apiToken)),
+      transport: transport
+    )
+    return (service, provider)
+  }
+
+  // MARK: - JSON builders
+
+  /// A single asset descriptor dictionary (all six fields).
+  internal static func assetDictionary(
+    fileChecksum: String,
+    downloadURL: String = "https://cvws.icloud-content.com/asset"
+  ) -> [String: Any] {
+    [
+      "fileChecksum": fileChecksum,
+      "size": 1_024,
+      "referenceChecksum": "ref-\(fileChecksum)",
+      "wrappingKey": "wk-\(fileChecksum)",
+      "receipt": "rcpt-\(fileChecksum)",
+      "downloadURL": downloadURL,
+    ]
+  }
+
+  /// `assets/rereference` 200 body wrapping the given per-item entries.
+  internal static func rereferenceResponse(assets: [[String: Any]]) throws -> ResponseConfig {
+    try jsonResponse(["assets": assets])
+  }
+
+  /// A `records/lookup` (or modify) 200 body wrapping one record.
+  internal static func recordsResponse(_ records: [[String: Any]]) throws -> ResponseConfig {
+    try jsonResponse(["records": records])
+  }
+
+  /// A "Note" record dictionary, optionally carrying an asset on `image`.
+  internal static func noteRecord(
+    recordName: String,
+    changeTag: String = "tag-1",
+    imageChecksum: String? = nil
+  ) -> [String: Any] {
+    var fields: [String: Any] = [
+      "title": ["value": "Note", "type": "STRING"]
+    ]
+    if let imageChecksum {
+      fields["image"] = [
+        "value": assetDictionary(fileChecksum: imageChecksum),
+        "type": "ASSETID",
+      ]
+    }
+    return [
+      "recordName": recordName,
+      "recordType": "Note",
+      "recordChangeTag": changeTag,
+      "fields": fields,
+    ]
+  }
+
+  /// A record dictionary that omits `recordType` (e.g. a tombstone-shaped
+  /// response), used to exercise the missing-`recordType` guard.
+  internal static func recordWithoutType(
+    recordName: String,
+    changeTag: String = "tag-1"
+  ) -> [String: Any] {
+    [
+      "recordName": recordName,
+      "recordChangeTag": changeTag,
+      "fields": ["title": ["value": "Note", "type": "STRING"]],
+    ]
+  }
+
+  private static func jsonResponse(_ object: [String: Any]) throws -> ResponseConfig {
+    let body = try JSONSerialization.data(withJSONObject: object)
+    var headers = HTTPFields()
+    headers[.contentType] = "application/json"
+    return ResponseConfig(statusCode: 200, headers: headers, body: body, error: nil)
+  }
+}
diff --git a/Tests/MistKitTests/CloudKitService/Rereference/CloudKitServiceTests.Rereference+SuccessCases.swift b/Tests/MistKitTests/CloudKitService/Rereference/CloudKitServiceTests.Rereference+SuccessCases.swift
new file mode 100644
index 00000000..50672658
--- /dev/null
+++ b/Tests/MistKitTests/CloudKitService/Rereference/CloudKitServiceTests.Rereference+SuccessCases.swift
@@ -0,0 +1,101 @@
+//
+//  CloudKitServiceTests.Rereference+SuccessCases.swift
+//  MistKit
+//
+//  Created by Leo Dion.
+//  Copyright © 2026 BrightDigit.
+//
+//  Permission is hereby granted, free of charge, to any person
+//  obtaining a copy of this software and associated documentation
+//  files (the "Software"), to deal in the Software without
+//  restriction, including without limitation the rights to use,
+//  copy, modify, merge, publish, distribute, sublicense, and/or
+//  sell copies of the Software, and to permit persons to whom the
+//  Software is furnished to do so, subject to the following
+//  conditions:
+//
+//  The above copyright notice and this permission notice shall be
+//  included in all copies or substantial portions of the Software.
+//
+//  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+//  EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+//  OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+//  NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+//  HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+//  WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+//  FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+//  OTHER DEALINGS IN THE SOFTWARE.
+//
+
+internal import Foundation
+internal import Testing
+
+@testable import MistKit
+
+extension CloudKitServiceTests.Rereference {
+  @Suite("Rereference Assets")
+  internal struct SuccessCases {
+    private typealias Helper = CloudKitServiceTests.Rereference
+
+    @Test("rereferenceAssets maps asset descriptors in order")
+    internal func mapsSuccessfulDescriptors() async throws {
+      guard #available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *) else {
+        Issue.record("CloudKitService is not available on this operating system.")
+        return
+      }
+      let service = try Helper.makeService(responsesByOperation: [
+        "rereferenceAssets": try Helper.rereferenceResponse(assets: [
+          Helper.assetDictionary(fileChecksum: "chk-1"),
+          Helper.assetDictionary(fileChecksum: "chk-2"),
+        ])
+      ])
+
+      let assets = try await service.rereferenceAssets(
+        [
+          (recordName: "note-a", fieldName: "image"),
+          (recordName: "note-b", fieldName: "image"),
+        ],
+        database: Helper.publicDatabase
+      )
+
+      #expect(assets.count == 2)
+      let first = try #require(assets.first)
+      #expect(first.fileChecksum == "chk-1")
+      #expect(first.referenceChecksum == "ref-chk-1")
+      #expect(first.wrappingKey == "wk-chk-1")
+      #expect(first.receipt == "rcpt-chk-1")
+      #expect(first.size == 1_024)
+      #expect(first.downloadURL == "https://cvws.icloud-content.com/asset")
+      #expect(assets.last?.fileChecksum == "chk-2")
+    }
+
+    @Test("rereferenceAssets throws on a top-level BAD_REQUEST")
+    internal func throwsOnTopLevelFailure() async throws {
+      guard #available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *) else {
+        Issue.record("CloudKitService is not available on this operating system.")
+        return
+      }
+      // The live service fails the whole request (HTTP 400) when a source
+      // record is missing, rather than returning a per-item error.
+      let service = try Helper.makeService(responsesByOperation: [
+        "rereferenceAssets": .cloudKitError(
+          statusCode: 400,
+          serverErrorCode: "BAD_REQUEST",
+          reason: "record to rereference does not exist"
+        )
+      ])
+
+      let error = await #expect(throws: CloudKitError.self) {
+        _ = try await service.rereferenceAssets(
+          [(recordName: "missing-rec", fieldName: "image")],
+          database: Helper.publicDatabase
+        )
+      }
+      guard case .badRequest(let reason) = error else {
+        Issue.record("Expected .badRequest, got \(String(describing: error))")
+        return
+      }
+      #expect(reason == "record to rereference does not exist")
+    }
+  }
+}
diff --git a/openapi.yaml b/openapi.yaml
index 341a03a0..992f0e8f 100644
--- a/openapi.yaml
+++ b/openapi.yaml
@@ -801,6 +801,65 @@ paths:
         '401':
           $ref: '#/components/responses/Failure'
 
+  /database/{version}/{container}/{environment}/{database}/assets/rereference:
+    post:
+      summary: Re-reference Existing Assets
+      description: |
+        Fetch reusable asset descriptors for assets that already live on other
+        records, without re-uploading the bytes. Each returned descriptor can be
+        set on another record's Asset field via `records/modify` to share the
+        same underlying asset. Assets are deleted only when all references to
+        them are removed.
+
+        Documented in Apple's archived CloudKit Web Services Reference
+        (`RereferenceAssets`); absent from the current online docs.
+      operationId: rereferenceAssets
+      tags:
+        - Assets
+      parameters:
+        - $ref: '#/components/parameters/version'
+        - $ref: '#/components/parameters/container'
+        - $ref: '#/components/parameters/environment'
+        - $ref: '#/components/parameters/database'
+      requestBody:
+        required: true
+        content:
+          application/json:
+            schema:
+              type: object
+              properties:
+                zoneID:
+                  $ref: '#/components/schemas/ZoneID'
+                  description: Optional zone ID. Defaults to default zone if not specified.
+                assets:
+                  type: array
+                  description: Array of source asset fields to re-reference.
+                  items:
+                    type: object
+                    required:
+                      - recordName
+                      - fieldName
+                    properties:
+                      recordName:
+                        type: string
+                        description: Name of the record holding the source asset.
+                      fieldName:
+                        type: string
+                        description: Name of the Asset field on the source record.
+              required:
+                - assets
+      responses:
+        '200':
+          description: Reusable asset descriptors returned successfully.
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/AssetRereferenceResponse'
+        '400':
+          $ref: '#/components/responses/Failure'
+        '401':
+          $ref: '#/components/responses/Failure'
+
   /device/{version}/{container}/{environment}/tokens/create:
     post:
       summary: Create APNs Token
@@ -1574,6 +1633,22 @@ components:
               fieldName:
                 type: string
 
+    AssetRereferenceResponse:
+      type: object
+      description: |
+        Response body for `assets/rereference`: one reusable asset descriptor
+        per requested asset field, wrapped under `assets`.
+
+        Verified against the live service: the endpoint validates atomically —
+        a bad entry (e.g. a missing source record) fails the *whole* request
+        with a top-level HTTP 400, so there are no inline per-item failures
+        here (unlike `ModifyResponse`/`LookupResponse`).
+      properties:
+        assets:
+          type: array
+          items:
+            $ref: '#/components/schemas/AssetValue'
+
     TokenResponse:
       type: object
       description: |

From e5863584f54b148c387b99e48edd2a6e6e7dc982 Mon Sep 17 00:00:00 2001
From: leogdion 
Date: Tue, 26 May 2026 14:51:12 -0400
Subject: [PATCH 27/35] setup-mistkit: pin to resolved revision (#380)

---
 README.md | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/README.md b/README.md
index c1f897e8..af82de56 100644
--- a/README.md
+++ b/README.md
@@ -13,7 +13,7 @@
 [![Maintainability](https://qlty.sh/badges/55637213-d307-477e-a710-f9dba332d955/maintainability.svg)](https://qlty.sh/gh/brightdigit/projects/MistKit)
 [![Documentation](https://img.shields.io/badge/docc-read_documentation-blue)](https://swiftpackageindex.com/brightdigit/MistKit/documentation)
 
-A Swift Package for Server-Side and Command-Line Access to [CloudKit Web Services](https://developer.apple.com/documentation/cloudkitwebservices)
+A Swift Package for Server-Side and Command-Line Access to [CloudKit Web Services](https://developer.apple.com/library/archive/documentation/DataManagement/Conceptual/CloudKitWebServicesReference/index.html)
 
 ## Table of Contents
 - [Overview](#overview)
@@ -330,7 +330,7 @@ Check out the `Examples/` directory for complete working examples:
 
 ### Apple References
 
-- **[CloudKit Web Services](https://developer.apple.com/documentation/cloudkitwebservices)**: Official CloudKit Web Services REST API documentation
+- **[CloudKit Web Services](https://developer.apple.com/library/archive/documentation/DataManagement/Conceptual/CloudKitWebServicesReference/index.html)**: Official CloudKit Web Services REST API documentation
 - **[CloudKit framework](https://developer.apple.com/documentation/cloudkit)**: On-device CloudKit framework (iOS/macOS)
 - **[CloudKit JS](https://developer.apple.com/documentation/cloudkitjs)**: Browser-based CloudKit access used for web auth token capture
 - **[CKFetchWebAuthTokenOperation](https://developer.apple.com/documentation/cloudkit/ckfetchwebauthtokenoperation)**: iOS/macOS API for exchanging an iCloud session for a web auth token

From a2b2f6a1779b9926bbd08cd92cd2caff3184acb1 Mon Sep 17 00:00:00 2001
From: Claude 
Date: Tue, 26 May 2026 19:18:11 +0000
Subject: [PATCH 28/35] Fix non-exhaustive CloudKitError switch in
 CelestraCloud

Handle the incompleteResponse, subscriptionOperationFailed, and
subscriptionLikelyDuplicate cases added to CloudKitError so the
CelestraCloud example builds. All three are classified as non-retriable.

https://claude.ai/code/session_01WY9rXuEkERRMMnLDz2872H
---
 .../Sources/CelestraCloudKit/Services/CelestraError.swift | 8 ++++++--
 1 file changed, 6 insertions(+), 2 deletions(-)

diff --git a/Examples/CelestraCloud/Sources/CelestraCloudKit/Services/CelestraError.swift b/Examples/CelestraCloud/Sources/CelestraCloudKit/Services/CelestraError.swift
index cdc6e089..cee7f485 100644
--- a/Examples/CelestraCloud/Sources/CelestraCloudKit/Services/CelestraError.swift
+++ b/Examples/CelestraCloud/Sources/CelestraCloudKit/Services/CelestraError.swift
@@ -147,8 +147,12 @@ public enum CelestraError: LocalizedError {
     case .unsupportedOperationType, .paginationLimitExceeded, .zonePaginationLimitExceeded:
       // Programmer/configuration issues — not retriable
       return false
-    case .conversionFailed, .recordOperationFailed:
-      // Response could not be mapped, or a per-record operation failed — not retriable
+    case .conversionFailed, .recordOperationFailed, .incompleteResponse:
+      // Response could not be mapped or was incomplete, or a per-record
+      // operation failed — not retriable
+      return false
+    case .subscriptionOperationFailed, .subscriptionLikelyDuplicate:
+      // Subscription operation failed or was a duplicate — not retriable
       return false
     case .missingCredentials, .invalidPrivateKey:
       // Credential/configuration issues — not retriable

From bd192783aa9819f89c5ea9ec9e85f91b87996df2 Mon Sep 17 00:00:00 2001
From: leogdion 
Date: Tue, 26 May 2026 19:55:52 -0400
Subject: [PATCH 29/35] Wire landed MistKit endpoints into MistDemo web app
 (#394) (#396)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* Wire landed MistKit endpoints into MistDemo web app (#394)

Replace the nine 501 "pending" stubs in MistKit-server mode with real
Hummingbird handlers that forward to the already-shipped CloudKitService
wrappers, restoring parity with CloudKit JS mode:

  records/lookup, records/changes, zones/list, zones/lookup, zones/changes,
  users/caller, users/discover, users/lookup/email, users/lookup/id

- WebBackend gains nine webXxx methods; CloudKitService conforms via thin
  forwards split across +Reads (records/zones) and +Users extensions.
- New request DTOs (WebRequests+Records/+Users, extended +Zones) and response
  DTOs (RecordChanges, ZoneChanges, Caller, Users). User routes carry no
  database selector — they run on the public DB with web-auth.
- records/lookup surfaces per-record failures via try get(), matching the
  webModifySubscriptions precedent.
- addUnwiredLandedEndpoints removed; WebServer+Pending now lists only
  records/resolve (#41, no wrapper yet).
- The browser already branched to these /api/* routes in MistKit mode, so no
  front-end change was needed — wiring the server flips them off the stub.

Tests: MockBackend extended with the nine methods + call captures; forwarding
and 401-unauthorized tests per route. 963 MistDemo tests pass; swift-format,
swiftlint, and header checks clean.

Co-Authored-By: Claude Opus 4.7 (1M context) 

* Address PR #396 review: discover-only user lookup + honest lookup semantics

Demo web app now exposes only the non-deprecated POST /users/discover for
user-identity lookup; the Apple-deprecated /users/lookup/email and
/users/lookup/id routes are removed from every layer (route, protocol,
conformance, DTO, mock, tests, frontend). Discover accepts emails AND user
record names, forwarded as UserIdentityLookupInfo entries; phone-number
support is tracked in #398.

webLookupRecords keeps its all-or-nothing behavior, but the comment now
honestly describes it (dropping the misleading "matches webModifySubscriptions"
claim) and a new test locks the semantic: a per-record backend failure surfaces
as 500, not a partial 200.

Co-Authored-By: Claude Opus 4.7 (1M context) 

---------

Co-authored-by: Claude Opus 4.7 (1M context) 
---
 .../Sources/MistDemoKit/Resources/index.html  |  25 +--
 .../Sources/MistDemoKit/Resources/js/users.js |  69 ++-----
 .../CloudKitService+WebBackend+Reads.swift    |  89 +++++++++
 .../CloudKitService+WebBackend+Users.swift    |  51 +++++
 .../MistDemoKit/Server/WebBackend.swift       |  32 +++
 .../Server/WebRequests+Records.swift          |  83 ++++++++
 .../Server/WebRequests+Users.swift            |  58 ++++++
 .../Server/WebRequests+Zones.swift            |  61 ++++++
 .../MistDemoKit/Server/WebResponse.swift      |  39 ++++
 .../Server/WebServer+Pending.swift            |  80 +-------
 .../Server/WebServer+Records.swift            | 101 ++++++++++
 .../MistDemoKit/Server/WebServer+Users.swift  |  95 +++++++++
 .../MistDemoKit/Server/WebServer+Zones.swift  |  88 ++++++++-
 .../MistDemoKit/Server/WebServer.swift        |   4 +-
 .../Server/MockBackend+Calls.swift            |  36 ++++
 .../Server/MockBackend+RecordOperations.swift |  78 ++++++++
 .../Server/MockBackend+UserOperations.swift   |  64 ++++++
 .../MistDemoTests/Server/MockBackend.swift    |   7 +
 .../Server/WebServerTests+Records.swift       | 165 ++++++++++++++++
 .../Server/WebServerTests+Users.swift         | 142 ++++++++++++++
 .../Server/WebServerTests+ZoneReads.swift     | 182 ++++++++++++++++++
 21 files changed, 1405 insertions(+), 144 deletions(-)
 create mode 100644 Examples/MistDemo/Sources/MistDemoKit/Server/CloudKitService+WebBackend+Reads.swift
 create mode 100644 Examples/MistDemo/Sources/MistDemoKit/Server/CloudKitService+WebBackend+Users.swift
 create mode 100644 Examples/MistDemo/Sources/MistDemoKit/Server/WebRequests+Records.swift
 create mode 100644 Examples/MistDemo/Sources/MistDemoKit/Server/WebRequests+Users.swift
 create mode 100644 Examples/MistDemo/Sources/MistDemoKit/Server/WebServer+Records.swift
 create mode 100644 Examples/MistDemo/Sources/MistDemoKit/Server/WebServer+Users.swift
 create mode 100644 Examples/MistDemo/Tests/MistDemoTests/Server/MockBackend+UserOperations.swift
 create mode 100644 Examples/MistDemo/Tests/MistDemoTests/Server/WebServerTests+Records.swift
 create mode 100644 Examples/MistDemo/Tests/MistDemoTests/Server/WebServerTests+Users.swift
 create mode 100644 Examples/MistDemo/Tests/MistDemoTests/Server/WebServerTests+ZoneReads.swift

diff --git a/Examples/MistDemo/Sources/MistDemoKit/Resources/index.html b/Examples/MistDemo/Sources/MistDemoKit/Resources/index.html
index a0cd5738..38e87299 100644
--- a/Examples/MistDemo/Sources/MistDemoKit/Resources/index.html
+++ b/Examples/MistDemo/Sources/MistDemoKit/Resources/index.html
@@ -245,9 +245,9 @@ 

Changes zones/changes

- +
-

Users users/caller · users/discover · users/lookup/email · users/lookup/id

+

Users users/caller · users/discover

Caller users/caller

@@ -258,27 +258,12 @@

Caller users/caller

(none yet)
-

Lookup by Email users/lookup/email

+

Discover users/discover (POST) (CloudKit JS loops per-item)

- - +
-
-
(none yet)
-
-
-

Lookup by Record Name users/lookup/id

-
- - -
-
-
(none yet)
-
-
-

Discover users/discover (POST) (CloudKit JS loops per-email)

- +
diff --git a/Examples/MistDemo/Sources/MistDemoKit/Resources/js/users.js b/Examples/MistDemo/Sources/MistDemoKit/Resources/js/users.js index 054858c0..47e518cc 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Resources/js/users.js +++ b/Examples/MistDemo/Sources/MistDemoKit/Resources/js/users.js @@ -1,14 +1,10 @@ -// users/caller · users/discover · users/lookup/email · users/lookup/id -// panel handlers. All four MistKit wrappers landed in #215 but aren't -// exposed on the demo server yet; CloudKit JS fully exercises every -// endpoint today. +// users/caller · users/discover panel handlers. The deprecated +// users/lookup/email and users/lookup/id primitives are not exposed — +// users/discover is Apple's supported replacement and handles both email +// and record-name lookups (phone-number support tracked in #398). const usersCallerStatus = document.getElementById('users-caller-status'); const usersCallerRaw = document.getElementById('users-caller-raw'); -const usersEmailStatus = document.getElementById('users-email-status'); -const usersEmailRaw = document.getElementById('users-email-raw'); -const usersIdStatus = document.getElementById('users-id-status'); -const usersIdRaw = document.getElementById('users-id-raw'); const usersDiscoverStatus = document.getElementById('users-discover-status'); const usersDiscoverRaw = document.getElementById('users-discover-raw'); @@ -26,48 +22,11 @@ document.getElementById('users-caller-btn').addEventListener('click', async () = }); }); -document.getElementById('users-email-btn').addEventListener('click', async () => { - const email = document.getElementById('users-email-input').value.trim(); - if (!email) { - setStatus(usersEmailStatus, 'Provide an email address.', 'error'); - return; - } - await runPanelOperation({ - statusEl: usersEmailStatus, - rawEl: usersEmailRaw, - label: 'Lookup by email', - fn: async () => { - if (currentMode === 'mistkit') { - return await postJSON('/api/users/lookup/email', { emails: [email] }); - } - return await ckJsContainer().discoverUserIdentityWithEmailAddress(email); - }, - }); -}); - -document.getElementById('users-id-btn').addEventListener('click', async () => { - const recordName = document.getElementById('users-id-input').value.trim(); - if (!recordName) { - setStatus(usersIdStatus, 'Provide a user record name.', 'error'); - return; - } - await runPanelOperation({ - statusEl: usersIdStatus, - rawEl: usersIdRaw, - label: 'Lookup by record name', - fn: async () => { - if (currentMode === 'mistkit') { - return await postJSON('/api/users/lookup/id', { userRecordNames: [recordName] }); - } - return await ckJsContainer().discoverUserIdentityWithUserRecordName(recordName); - }, - }); -}); - document.getElementById('users-discover-btn').addEventListener('click', async () => { - const emails = csv(document.getElementById('users-discover-input').value); - if (emails.length === 0) { - setStatus(usersDiscoverStatus, 'Provide at least one email.', 'error'); + const emails = csv(document.getElementById('users-discover-emails').value); + const userRecordNames = csv(document.getElementById('users-discover-record-names').value); + if (emails.length === 0 && userRecordNames.length === 0) { + setStatus(usersDiscoverStatus, 'Provide at least one email or record name.', 'error'); return; } await runPanelOperation({ @@ -76,9 +35,9 @@ document.getElementById('users-discover-btn').addEventListener('click', async () label: 'Discover users', fn: async () => { if (currentMode === 'mistkit') { - return await postJSON('/api/users/discover', { emails }); + return await postJSON('/api/users/discover', { emails, userRecordNames }); } - // CloudKit JS exposes a per-email primitive — loop and aggregate + // CloudKit JS exposes per-item primitives — loop and aggregate // to match the REST endpoint's batch shape. const results = []; for (const email of emails) { @@ -89,6 +48,14 @@ document.getElementById('users-discover-btn').addEventListener('click', async () results.push({ email, error: error.message }); } } + for (const recordName of userRecordNames) { + try { + const identity = await ckJsContainer().discoverUserIdentityWithUserRecordName(recordName); + results.push({ userRecordName: recordName, identity }); + } catch (error) { + results.push({ userRecordName: recordName, error: error.message }); + } + } return { discovered: results }; }, }); diff --git a/Examples/MistDemo/Sources/MistDemoKit/Server/CloudKitService+WebBackend+Reads.swift b/Examples/MistDemo/Sources/MistDemoKit/Server/CloudKitService+WebBackend+Reads.swift new file mode 100644 index 00000000..4c12fc8a --- /dev/null +++ b/Examples/MistDemo/Sources/MistDemoKit/Server/CloudKitService+WebBackend+Reads.swift @@ -0,0 +1,89 @@ +// +// CloudKitService+WebBackend+Reads.swift +// MistDemo +// +// Created by Leo Dion. +// Copyright © 2026 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +internal import Foundation +internal import MistKit + +// Read-side `WebBackend` conformance for records and zones: lookup, changes, +// and zone listing. The primary conformance declaration lives in +// `CloudKitService+WebBackend.swift`. +extension CloudKitService { + internal func webLookupRecords( + recordNames: [String], + database: MistKit.Database + ) async throws -> [RecordInfo] { + let results = try await lookupRecords( + recordNames: recordNames, + desiredKeys: nil, + database: database + ) + // All-or-nothing: `lookupRecords` returns a per-record `[RecordResult]`, + // but the demo collapses it — any single failure (e.g. CloudKit's + // NOT_FOUND) throws, so the web panel shows the error rather than + // silently returning fewer rows than were asked for. Surfacing partial + // results (found records alongside per-record failures) is a possible + // future enhancement. + return try results.map { try $0.get() } + } + + internal func webRecordChanges( + zoneName: String?, + syncToken: String?, + database: MistKit.Database + ) async throws -> RecordChangesResult { + try await fetchRecordChanges( + zoneID: zoneName.map { ZoneID(zoneName: $0) }, + syncToken: syncToken, + database: database + ) + } + + internal func webListZones( + database: MistKit.Database + ) async throws -> [ZoneInfo] { + try await listZones(database: database) + } + + internal func webLookupZones( + zoneNames: [String], + database: MistKit.Database + ) async throws -> [ZoneInfo] { + try await lookupZones( + zoneIDs: zoneNames.map { ZoneID(zoneName: $0) }, + database: database + ) + } + + internal func webZoneChanges( + syncToken: String?, + database: MistKit.Database + ) async throws -> ZoneChangesResult { + try await fetchZoneChanges(syncToken: syncToken, database: database) + } +} diff --git a/Examples/MistDemo/Sources/MistDemoKit/Server/CloudKitService+WebBackend+Users.swift b/Examples/MistDemo/Sources/MistDemoKit/Server/CloudKitService+WebBackend+Users.swift new file mode 100644 index 00000000..9704fd2b --- /dev/null +++ b/Examples/MistDemo/Sources/MistDemoKit/Server/CloudKitService+WebBackend+Users.swift @@ -0,0 +1,51 @@ +// +// CloudKitService+WebBackend+Users.swift +// MistDemo +// +// Created by Leo Dion. +// Copyright © 2026 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +internal import Foundation +internal import MistKit + +// User-identity `WebBackend` conformance. These operate on the public +// database with web-auth credentials, so none take a `database` argument. +// The primary conformance declaration lives in +// `CloudKitService+WebBackend.swift`. +extension CloudKitService { + internal func webFetchCaller() async throws -> UserInfo { + try await fetchCaller() + } + + internal func webDiscoverUsers( + emails: [String], + userRecordNames: [String] + ) async throws -> [UserIdentity] { + let lookupInfos = + emails.map { UserIdentityLookupInfo(emailAddress: $0) } + + userRecordNames.map { UserIdentityLookupInfo(userRecordName: $0) } + return try await discoverUserIdentities(lookupInfos: lookupInfos) + } +} diff --git a/Examples/MistDemo/Sources/MistDemoKit/Server/WebBackend.swift b/Examples/MistDemo/Sources/MistDemoKit/Server/WebBackend.swift index 7687cebd..2669072b 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Server/WebBackend.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Server/WebBackend.swift @@ -68,12 +68,44 @@ internal protocol WebBackend: Sendable { database: MistKit.Database ) async throws + func webLookupRecords( + recordNames: [String], + database: MistKit.Database + ) async throws -> [RecordInfo] + + func webRecordChanges( + zoneName: String?, + syncToken: String?, + database: MistKit.Database + ) async throws -> RecordChangesResult + func webModifyZones( create: [String], delete: [String], database: MistKit.Database ) async throws -> [ZoneInfo] + func webListZones( + database: MistKit.Database + ) async throws -> [ZoneInfo] + + func webLookupZones( + zoneNames: [String], + database: MistKit.Database + ) async throws -> [ZoneInfo] + + func webZoneChanges( + syncToken: String?, + database: MistKit.Database + ) async throws -> ZoneChangesResult + + func webFetchCaller() async throws -> UserInfo + + func webDiscoverUsers( + emails: [String], + userRecordNames: [String] + ) async throws -> [UserIdentity] + func webListSubscriptions( database: MistKit.Database ) async throws -> [SubscriptionInfo] diff --git a/Examples/MistDemo/Sources/MistDemoKit/Server/WebRequests+Records.swift b/Examples/MistDemo/Sources/MistDemoKit/Server/WebRequests+Records.swift new file mode 100644 index 00000000..e31bd6c3 --- /dev/null +++ b/Examples/MistDemo/Sources/MistDemoKit/Server/WebRequests+Records.swift @@ -0,0 +1,83 @@ +// +// WebRequests+Records.swift +// MistDemo +// +// Created by Leo Dion. +// Copyright © 2026 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +internal import Foundation +internal import MistKit + +extension WebRequests { + /// `POST /api/records/lookup` — fetch specific records by name. Mirrors + /// CloudKit Web Services `records/lookup` / CloudKit JS `fetchRecords`. + internal struct Lookup: Decodable { + private enum CodingKeys: String, CodingKey { + case recordNames + case database + } + + internal let recordNames: [String] + internal let database: MistKit.Database + + internal init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.recordNames = + try container.decodeIfPresent([String].self, forKey: .recordNames) + ?? [] + self.database = try WebRequests.decodeDatabase( + from: container, forKey: .database + ) + } + } + + /// `POST /api/records/changes` — record changes in a zone since an optional + /// continuation `syncToken`. The browser defaults `zoneName` to + /// `_defaultZone`; omitting it lets the MistKit wrapper use its default. + internal struct RecordChanges: Decodable { + private enum CodingKeys: String, CodingKey { + case zoneName + case syncToken + case database + } + + internal let zoneName: String? + internal let syncToken: String? + internal let database: MistKit.Database + + internal init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.zoneName = try container.decodeIfPresent( + String.self, forKey: .zoneName + ) + self.syncToken = try container.decodeIfPresent( + String.self, forKey: .syncToken + ) + self.database = try WebRequests.decodeDatabase( + from: container, forKey: .database + ) + } + } +} diff --git a/Examples/MistDemo/Sources/MistDemoKit/Server/WebRequests+Users.swift b/Examples/MistDemo/Sources/MistDemoKit/Server/WebRequests+Users.swift new file mode 100644 index 00000000..46fd42d4 --- /dev/null +++ b/Examples/MistDemo/Sources/MistDemoKit/Server/WebRequests+Users.swift @@ -0,0 +1,58 @@ +// +// WebRequests+Users.swift +// MistDemo +// +// Created by Leo Dion. +// Copyright © 2026 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +internal import Foundation + +// User-identity routes have no `database` field: the underlying MistKit +// wrapper (`discoverUserIdentities`) operates on the public database with +// web-auth credentials regardless of the request's selected database. +extension WebRequests { + /// `POST /api/users/discover` — discover user identities by email address + /// and/or user record name. Either list may be omitted; an absent key + /// decodes to an empty array. + internal struct DiscoverUsers: Decodable { + private enum CodingKeys: String, CodingKey { + case emails + case userRecordNames + } + + internal let emails: [String] + internal let userRecordNames: [String] + + internal init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.emails = + try container.decodeIfPresent([String].self, forKey: .emails) ?? [] + self.userRecordNames = + try container.decodeIfPresent( + [String].self, forKey: .userRecordNames + ) ?? [] + } + } +} diff --git a/Examples/MistDemo/Sources/MistDemoKit/Server/WebRequests+Zones.swift b/Examples/MistDemo/Sources/MistDemoKit/Server/WebRequests+Zones.swift index ba324121..2ba551a5 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Server/WebRequests+Zones.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Server/WebRequests+Zones.swift @@ -66,4 +66,65 @@ extension WebRequests { ) } } + + /// `POST /api/zones/list` — every zone in the target database. Carries only + /// the database selector; `zones/list` (GET in CloudKit Web Services) takes + /// no further input. + internal struct ListZones: Decodable { + private enum CodingKeys: String, CodingKey { + case database + } + + internal let database: MistKit.Database + + internal init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.database = try WebRequests.decodeDatabase( + from: container, forKey: .database + ) + } + } + + /// `POST /api/zones/lookup` — resolve specific zones by name. The browser + /// sends bare zone names; the backend maps them to `ZoneID` values. + internal struct LookupZones: Decodable { + private enum CodingKeys: String, CodingKey { + case zoneNames + case database + } + + internal let zoneNames: [String] + internal let database: MistKit.Database + + internal init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.zoneNames = + try container.decodeIfPresent([String].self, forKey: .zoneNames) ?? [] + self.database = try WebRequests.decodeDatabase( + from: container, forKey: .database + ) + } + } + + /// `POST /api/zones/changes` — database-level zone changes since an optional + /// continuation `syncToken`. + internal struct ZoneChanges: Decodable { + private enum CodingKeys: String, CodingKey { + case syncToken + case database + } + + internal let syncToken: String? + internal let database: MistKit.Database + + internal init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.syncToken = try container.decodeIfPresent( + String.self, forKey: .syncToken + ) + self.database = try WebRequests.decodeDatabase( + from: container, forKey: .database + ) + } + } } diff --git a/Examples/MistDemo/Sources/MistDemoKit/Server/WebResponse.swift b/Examples/MistDemo/Sources/MistDemoKit/Server/WebResponse.swift index 103bb91b..d75a0782 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Server/WebResponse.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Server/WebResponse.swift @@ -43,6 +43,45 @@ internal enum WebResponse { internal let deleted: Bool } + /// Body returned by `records/changes`: the changed records plus the + /// continuation `syncToken` and `moreComing` flag from + /// `RecordChangesResult`. + internal struct RecordChanges: Encodable { + internal let records: [RecordInfo] + internal let syncToken: String? + internal let moreComing: Bool + + internal init(from result: RecordChangesResult) { + self.records = result.records + self.syncToken = result.syncToken + self.moreComing = result.moreComing + } + } + + /// Body returned by `zones/changes`: the changed zones plus the continuation + /// `syncToken` and `moreComing` flag from `ZoneChangesResult`. + internal struct ZoneChanges: Encodable { + internal let zones: [ZoneInfo] + internal let syncToken: String? + internal let moreComing: Bool + + internal init(from result: ZoneChangesResult) { + self.zones = result.zones + self.syncToken = result.syncToken + self.moreComing = result.moreComing + } + } + + /// Body returned by `users/caller`: the calling user's `UserInfo`. + internal struct Caller: Encodable { + internal let user: UserInfo + } + + /// Body returned by the user-identity discover route (`users/discover`). + internal struct Users: Encodable { + internal let users: [UserIdentity] + } + /// Body returned by zone routes (`zones/modify`). `ZoneInfo` encodes to /// `{ zoneName, ownerRecordName, capabilities }`, which the browser's /// zone table reads directly. diff --git a/Examples/MistDemo/Sources/MistDemoKit/Server/WebServer+Pending.swift b/Examples/MistDemo/Sources/MistDemoKit/Server/WebServer+Pending.swift index e598f975..399f5fa2 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Server/WebServer+Pending.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Server/WebServer+Pending.swift @@ -33,15 +33,8 @@ internal import Hummingbird extension WebServer { - /// HTTP verbs the pending registrar supports — only the ones we need. - internal enum PendingVerb { - case get - case post - } - - private static func registerPending( + private static func registerPendingPost( api: RouterGroup, - verb: PendingVerb, path: String, endpoint: String, trackingIssue: Int @@ -59,16 +52,9 @@ "Failed to encode pending-stub body for \(endpoint): \(error)" ) } - let handler: @Sendable (Request, BasicRequestContext) async throws -> Response = { - _, _ -> Response in + api.post(RouterPath(path)) { _, _ -> Response in Self.jsonResponse(status: .notImplemented, bytes: bytes) } - switch verb { - case .get: - api.get(RouterPath(path), use: handler) - case .post: - api.post(RouterPath(path), use: handler) - } } /// Register 501 stubs for every CloudKit Web Services endpoint not yet @@ -78,72 +64,24 @@ /// corresponding `api.(...)` registration here to the real handler /// (or move it into a dedicated extension file). /// - /// Remaining pending calls fall into two groups: - /// - /// 1. **No MistKit wrapper yet** (registered below): - /// - `POST records/resolve` (#41) — `CloudKitService` has no - /// `resolveRecords`; `ResolveCommand` likewise only prints a stub. - /// - /// 2. **Wrapper landed, route wiring outstanding** (#394, registered in - /// ``addUnwiredLandedEndpoints(api:)``): `records/lookup`, - /// `records/changes`, `zones/list`, `zones/lookup`, `zones/changes`, - /// `users/caller`, `users/discover`, `users/lookup/email`, - /// `users/lookup/id`. + /// Only one endpoint remains pending — it has **no MistKit wrapper yet**: + /// - `POST records/resolve` (#41) — `CloudKitService` has no + /// `resolveRecords`; `ResolveCommand` likewise only prints a stub. /// /// Already moved off this list to real handlers: `subscriptions/*` /// (#49/#50/#51 → `WebServer+Subscriptions`), `tokens/*` - /// (#52/#53 → `WebServer+Tokens`), and `assets/rereference` - /// (#31 → `WebServer+Assets`). + /// (#52/#53 → `WebServer+Tokens`), `assets/rereference` + /// (#31 → `WebServer+Assets`), and the records/zones/users endpoints + /// (#394 → `WebServer+Records` / `WebServer+Zones` / `WebServer+Users`). internal func addPendingEndpoints( api: RouterGroup ) { - Self.registerPending( + Self.registerPendingPost( api: api, - verb: .post, path: "records/resolve", endpoint: "records/resolve", trackingIssue: 41 ) - - addUnwiredLandedEndpoints(api: api) - } - - /// Register 501 stubs for endpoints whose MistKit Swift wrapper *has* - /// already landed but isn't exposed on the demo server yet — only the - /// `/api/*` route wiring is outstanding, tracked by #394. These return - /// the same structured pending body as `addPendingEndpoints`; replacing a - /// stub here with a real handler (mirroring `WebServer+Zones`) is the - /// remaining work for #394's "exercisable in both modes" criterion. - /// - /// Note: #394 is the tracking issue for the *route wiring*, not for the - /// wrapper (which shipped under #215/#45/#47/#48/#367); #370 only - /// scaffolded these as stubs. - internal func addUnwiredLandedEndpoints( - api: RouterGroup - ) { - let routeWiringIssue = 394 - // Each route's `endpoint` label equals its path here, so a flat list of - // (verb, path) pairs drives registration without per-route boilerplate. - let routes: [(verb: PendingVerb, path: String)] = [ - (.post, "records/lookup"), - (.post, "records/changes"), - (.post, "zones/list"), - (.post, "zones/lookup"), - (.post, "zones/changes"), - (.get, "users/caller"), - (.post, "users/discover"), - (.post, "users/lookup/email"), - (.post, "users/lookup/id"), - ] - for route in routes { - Self.registerPending( - api: api, - verb: route.verb, - path: route.path, - endpoint: route.path, - trackingIssue: routeWiringIssue - ) - } } } #endif diff --git a/Examples/MistDemo/Sources/MistDemoKit/Server/WebServer+Records.swift b/Examples/MistDemo/Sources/MistDemoKit/Server/WebServer+Records.swift new file mode 100644 index 00000000..b5ad1434 --- /dev/null +++ b/Examples/MistDemo/Sources/MistDemoKit/Server/WebServer+Records.swift @@ -0,0 +1,101 @@ +// +// WebServer+Records.swift +// MistDemo +// +// Created by Leo Dion. +// Copyright © 2026 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +#if canImport(Hummingbird) + internal import Foundation + internal import Hummingbird + internal import MistKit + + extension WebServer { + /// Register the record routes wired to landed MistKit wrappers: + /// `lookup` and `changes`. (`query`/`create`/`update`/`delete` are + /// registered elsewhere; `resolve` stays a pending stub — see #41.) + internal func addRecordsEndpoints( + api: RouterGroup + ) { + addRecordsLookupEndpoint(api: api) + addRecordsChangesEndpoint(api: api) + } + + /// `POST /api/records/lookup` — fetch specific records by name, mirroring + /// CloudKit JS mode's `fetchRecords`. + private func addRecordsLookupEndpoint( + api: RouterGroup + ) { + let tokenStore = self.tokenStore + let backendFactory = self.backendFactory + api.post("records/lookup") { request, context -> Response in + guard let token = await tokenStore.currentToken else { + return Response(status: .unauthorized) + } + let body = try await request.decode( + as: WebRequests.Lookup.self, context: context + ) + return try await Self.runOperation { () -> Data in + let backend = try backendFactory.make(token) + let records = try await backend.webLookupRecords( + recordNames: body.recordNames, + database: body.database + ) + return try WebJSON.encoder().encode( + WebResponse.Records(records: records) + ) + } + } + } + + /// `POST /api/records/changes` — record changes in a zone since an + /// optional continuation `syncToken`. + private func addRecordsChangesEndpoint( + api: RouterGroup + ) { + let tokenStore = self.tokenStore + let backendFactory = self.backendFactory + api.post("records/changes") { request, context -> Response in + guard let token = await tokenStore.currentToken else { + return Response(status: .unauthorized) + } + let body = try await request.decode( + as: WebRequests.RecordChanges.self, context: context + ) + return try await Self.runOperation { () -> Data in + let backend = try backendFactory.make(token) + let result = try await backend.webRecordChanges( + zoneName: body.zoneName, + syncToken: body.syncToken, + database: body.database + ) + return try WebJSON.encoder().encode( + WebResponse.RecordChanges(from: result) + ) + } + } + } + } +#endif diff --git a/Examples/MistDemo/Sources/MistDemoKit/Server/WebServer+Users.swift b/Examples/MistDemo/Sources/MistDemoKit/Server/WebServer+Users.swift new file mode 100644 index 00000000..977c1d7b --- /dev/null +++ b/Examples/MistDemo/Sources/MistDemoKit/Server/WebServer+Users.swift @@ -0,0 +1,95 @@ +// +// WebServer+Users.swift +// MistDemo +// +// Created by Leo Dion. +// Copyright © 2026 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +#if canImport(Hummingbird) + internal import Foundation + internal import Hummingbird + internal import MistKit + + extension WebServer { + /// Register the user-identity routes: `caller` and `discover`. Both + /// operate on the public database with web-auth credentials, so neither + /// carries a `database` selector. The deprecated `lookup/email` and + /// `lookup/id` primitives are intentionally not exposed — `discover` is + /// Apple's supported replacement and handles email + record-name lookups. + internal func addUsersEndpoints( + api: RouterGroup + ) { + addUsersCallerEndpoint(api: api) + addUsersDiscoverEndpoint(api: api) + } + + /// `GET /api/users/caller` — the calling user's identity. + private func addUsersCallerEndpoint( + api: RouterGroup + ) { + let tokenStore = self.tokenStore + let backendFactory = self.backendFactory + api.get("users/caller") { _, _ -> Response in + guard let token = await tokenStore.currentToken else { + return Response(status: .unauthorized) + } + return try await Self.runOperation { () -> Data in + let backend = try backendFactory.make(token) + let user = try await backend.webFetchCaller() + return try WebJSON.encoder().encode( + WebResponse.Caller(user: user) + ) + } + } + } + + /// `POST /api/users/discover` — discover user identities by email + /// address and/or user record name. + private func addUsersDiscoverEndpoint( + api: RouterGroup + ) { + let tokenStore = self.tokenStore + let backendFactory = self.backendFactory + api.post("users/discover") { request, context -> Response in + guard let token = await tokenStore.currentToken else { + return Response(status: .unauthorized) + } + let body = try await request.decode( + as: WebRequests.DiscoverUsers.self, context: context + ) + return try await Self.runOperation { () -> Data in + let backend = try backendFactory.make(token) + let users = try await backend.webDiscoverUsers( + emails: body.emails, + userRecordNames: body.userRecordNames + ) + return try WebJSON.encoder().encode( + WebResponse.Users(users: users) + ) + } + } + } + } +#endif diff --git a/Examples/MistDemo/Sources/MistDemoKit/Server/WebServer+Zones.swift b/Examples/MistDemo/Sources/MistDemoKit/Server/WebServer+Zones.swift index 2fd330d1..ff428922 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Server/WebServer+Zones.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Server/WebServer+Zones.swift @@ -33,11 +33,21 @@ internal import MistKit extension WebServer { + /// Register every zone route: `modify`, `list`, `lookup`, and `changes`. + internal func addZonesEndpoints( + api: RouterGroup + ) { + addZonesModifyEndpoint(api: api) + addZonesListEndpoint(api: api) + addZonesLookupEndpoint(api: api) + addZonesChangesEndpoint(api: api) + } + /// `POST /api/zones/modify` — create and/or delete zones in one batch, /// backing the demo's MistKit-mode "Create Zone" / "Delete Zone" buttons. /// CloudKit JS mode hits the browser SDK directly; this is the /// server-side counterpart. - internal func addZonesModifyEndpoint( + private func addZonesModifyEndpoint( api: RouterGroup ) { let tokenStore = self.tokenStore @@ -62,5 +72,81 @@ } } } + + /// `POST /api/zones/list` — every zone in the target database. + private func addZonesListEndpoint( + api: RouterGroup + ) { + let tokenStore = self.tokenStore + let backendFactory = self.backendFactory + api.post("zones/list") { request, context -> Response in + guard let token = await tokenStore.currentToken else { + return Response(status: .unauthorized) + } + let body = try await request.decode( + as: WebRequests.ListZones.self, context: context + ) + return try await Self.runOperation { () -> Data in + let backend = try backendFactory.make(token) + let zones = try await backend.webListZones(database: body.database) + return try WebJSON.encoder().encode( + WebResponse.Zones(zones: zones) + ) + } + } + } + + /// `POST /api/zones/lookup` — resolve specific zones by name. + private func addZonesLookupEndpoint( + api: RouterGroup + ) { + let tokenStore = self.tokenStore + let backendFactory = self.backendFactory + api.post("zones/lookup") { request, context -> Response in + guard let token = await tokenStore.currentToken else { + return Response(status: .unauthorized) + } + let body = try await request.decode( + as: WebRequests.LookupZones.self, context: context + ) + return try await Self.runOperation { () -> Data in + let backend = try backendFactory.make(token) + let zones = try await backend.webLookupZones( + zoneNames: body.zoneNames, + database: body.database + ) + return try WebJSON.encoder().encode( + WebResponse.Zones(zones: zones) + ) + } + } + } + + /// `POST /api/zones/changes` — database-level zone changes since an + /// optional continuation `syncToken`. + private func addZonesChangesEndpoint( + api: RouterGroup + ) { + let tokenStore = self.tokenStore + let backendFactory = self.backendFactory + api.post("zones/changes") { request, context -> Response in + guard let token = await tokenStore.currentToken else { + return Response(status: .unauthorized) + } + let body = try await request.decode( + as: WebRequests.ZoneChanges.self, context: context + ) + return try await Self.runOperation { () -> Data in + let backend = try backendFactory.make(token) + let result = try await backend.webZoneChanges( + syncToken: body.syncToken, + database: body.database + ) + return try WebJSON.encoder().encode( + WebResponse.ZoneChanges(from: result) + ) + } + } + } } #endif diff --git a/Examples/MistDemo/Sources/MistDemoKit/Server/WebServer.swift b/Examples/MistDemo/Sources/MistDemoKit/Server/WebServer.swift index db4c491e..686ff9ec 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Server/WebServer.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Server/WebServer.swift @@ -127,7 +127,9 @@ addCreateEndpoint(api: api) addUpdateEndpoint(api: api) addDeleteEndpoint(api: api) - addZonesModifyEndpoint(api: api) + addRecordsEndpoints(api: api) + addZonesEndpoints(api: api) + addUsersEndpoints(api: api) addSubscriptionEndpoints(api: api) addTokenEndpoints(api: api) addAssetEndpoints(api: api) diff --git a/Examples/MistDemo/Tests/MistDemoTests/Server/MockBackend+Calls.swift b/Examples/MistDemo/Tests/MistDemoTests/Server/MockBackend+Calls.swift index 0bc57401..299c1ad3 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Server/MockBackend+Calls.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Server/MockBackend+Calls.swift @@ -67,6 +67,19 @@ internal let database: MistKit.Database } + /// Captured arguments from the most recent `webLookupRecords` call. + internal struct LookupRecordsCall: Sendable { + internal let recordNames: [String] + internal let database: MistKit.Database + } + + /// Captured arguments from the most recent `webRecordChanges` call. + internal struct RecordChangesCall: Sendable { + internal let zoneName: String? + internal let syncToken: String? + internal let database: MistKit.Database + } + /// Captured arguments from the most recent `webModifyZones` call. internal struct ModifyZonesCall: Sendable { internal let create: [String] @@ -74,6 +87,29 @@ internal let database: MistKit.Database } + /// Captured arguments from the most recent `webListZones` call. + internal struct ListZonesCall: Sendable { + internal let database: MistKit.Database + } + + /// Captured arguments from the most recent `webLookupZones` call. + internal struct LookupZonesCall: Sendable { + internal let zoneNames: [String] + internal let database: MistKit.Database + } + + /// Captured arguments from the most recent `webZoneChanges` call. + internal struct ZoneChangesCall: Sendable { + internal let syncToken: String? + internal let database: MistKit.Database + } + + /// Captured arguments from the most recent `webDiscoverUsers` call. + internal struct DiscoverUsersCall: Sendable { + internal let emails: [String] + internal let userRecordNames: [String] + } + /// Captured arguments from the most recent `webLookupSubscriptions` call. internal struct LookupSubscriptionsCall: Sendable { internal let ids: [String] diff --git a/Examples/MistDemo/Tests/MistDemoTests/Server/MockBackend+RecordOperations.swift b/Examples/MistDemo/Tests/MistDemoTests/Server/MockBackend+RecordOperations.swift index a5629477..aa2c3a4c 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Server/MockBackend+RecordOperations.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Server/MockBackend+RecordOperations.swift @@ -104,6 +104,38 @@ try consumePendingError() } + internal func webLookupRecords( + recordNames: [String], + database: MistKit.Database + ) async throws -> [RecordInfo] { + lastLookupRecords = LookupRecordsCall( + recordNames: recordNames, + database: database + ) + try consumePendingError() + return recordNames.map { name in + Self.stubRecord(recordType: "Note", recordName: name) + } + } + + internal func webRecordChanges( + zoneName: String?, + syncToken: String?, + database: MistKit.Database + ) async throws -> RecordChangesResult { + lastRecordChanges = RecordChangesCall( + zoneName: zoneName, + syncToken: syncToken, + database: database + ) + try consumePendingError() + return RecordChangesResult( + records: [Self.stubRecord(recordType: "Note", recordName: "changed-1")], + syncToken: "stub-record-sync-token", + moreComing: false + ) + } + internal func webModifyZones( create: [String], delete: [String], @@ -119,5 +151,51 @@ ZoneInfo(zoneName: name, ownerRecordName: nil, capabilities: []) } } + + internal func webListZones( + database: MistKit.Database + ) async throws -> [ZoneInfo] { + lastListZones = ListZonesCall(database: database) + try consumePendingError() + return [ + ZoneInfo( + zoneName: "_defaultZone", ownerRecordName: nil, capabilities: [] + ) + ] + } + + internal func webLookupZones( + zoneNames: [String], + database: MistKit.Database + ) async throws -> [ZoneInfo] { + lastLookupZones = LookupZonesCall( + zoneNames: zoneNames, + database: database + ) + try consumePendingError() + return zoneNames.map { name in + ZoneInfo(zoneName: name, ownerRecordName: nil, capabilities: []) + } + } + + internal func webZoneChanges( + syncToken: String?, + database: MistKit.Database + ) async throws -> ZoneChangesResult { + lastZoneChanges = ZoneChangesCall( + syncToken: syncToken, + database: database + ) + try consumePendingError() + return ZoneChangesResult( + zones: [ + ZoneInfo( + zoneName: "_defaultZone", ownerRecordName: nil, capabilities: [] + ) + ], + syncToken: "stub-zone-sync-token", + moreComing: false + ) + } } #endif diff --git a/Examples/MistDemo/Tests/MistDemoTests/Server/MockBackend+UserOperations.swift b/Examples/MistDemo/Tests/MistDemoTests/Server/MockBackend+UserOperations.swift new file mode 100644 index 00000000..8b9861ef --- /dev/null +++ b/Examples/MistDemo/Tests/MistDemoTests/Server/MockBackend+UserOperations.swift @@ -0,0 +1,64 @@ +// +// MockBackend+UserOperations.swift +// MistDemoTests +// +// Created by Leo Dion. +// Copyright © 2026 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +#if canImport(Hummingbird) + internal import MistKit + + @testable import MistDemoKit + + extension MockBackend { + internal func webFetchCaller() async throws -> UserInfo { + didFetchCaller = true + try consumePendingError() + return UserInfo( + userRecordName: "stub-caller", + firstName: "Stub", + lastName: "Caller", + emailAddress: "stub@example.com" + ) + } + + internal func webDiscoverUsers( + emails: [String], + userRecordNames: [String] + ) async throws -> [UserIdentity] { + lastDiscoverUsers = DiscoverUsersCall( + emails: emails, + userRecordNames: userRecordNames + ) + try consumePendingError() + return emails.map { email in + UserIdentity(lookupInfo: UserIdentityLookupInfo(emailAddress: email)) + } + + userRecordNames.map { name in + UserIdentity(userRecordName: .recordName(name)) + } + } + } +#endif diff --git a/Examples/MistDemo/Tests/MistDemoTests/Server/MockBackend.swift b/Examples/MistDemo/Tests/MistDemoTests/Server/MockBackend.swift index 480f7850..ef7baeb6 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Server/MockBackend.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Server/MockBackend.swift @@ -46,7 +46,14 @@ internal var lastCreate: CreateCall? internal var lastUpdate: UpdateCall? internal var lastDelete: DeleteCall? + internal var lastLookupRecords: LookupRecordsCall? + internal var lastRecordChanges: RecordChangesCall? internal var lastModifyZones: ModifyZonesCall? + internal var lastListZones: ListZonesCall? + internal var lastLookupZones: LookupZonesCall? + internal var lastZoneChanges: ZoneChangesCall? + internal var didFetchCaller = false + internal var lastDiscoverUsers: DiscoverUsersCall? internal var didListSubscriptions = false internal var lastLookupSubscriptions: LookupSubscriptionsCall? internal var lastModifySubscriptions: ModifySubscriptionsCall? diff --git a/Examples/MistDemo/Tests/MistDemoTests/Server/WebServerTests+Records.swift b/Examples/MistDemo/Tests/MistDemoTests/Server/WebServerTests+Records.swift new file mode 100644 index 00000000..b3ad75a8 --- /dev/null +++ b/Examples/MistDemo/Tests/MistDemoTests/Server/WebServerTests+Records.swift @@ -0,0 +1,165 @@ +// +// WebServerTests+Records.swift +// MistDemoTests +// +// Created by Leo Dion. +// Copyright © 2026 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +#if canImport(Hummingbird) + internal import Foundation + internal import HTTPTypes + internal import Hummingbird + internal import HummingbirdTesting + internal import MistKit + internal import Testing + + @testable import MistDemoKit + + extension WebServerTests { + private struct RecordsPayload: Decodable { + let records: [RecordInfo] + } + + private struct RecordChangesPayload: Decodable { + let records: [RecordInfo] + let syncToken: String? + let moreComing: Bool + } + + @Test("POST /api/records/lookup forwards record names to the backend") + internal func recordsLookupForwards() async throws { + let fixture = Self.makeFixture(authenticated: true) + let app = Application(router: try fixture.server.makeRouter()) + let jsonBody = #"{"database":"private","recordNames":["note-1","note-2"]}"# + + try await app.test(.router) { client in + try await client.execute( + uri: "/api/records/lookup", + method: .post, + headers: [.contentType: "application/json"], + body: ByteBuffer(string: jsonBody) + ) { response in + #expect(response.status == .ok) + let payload = try JSONDecoder().decode( + RecordsPayload.self, + from: Data(response.body.readableBytesView) + ) + #expect(payload.records.map(\.recordName) == ["note-1", "note-2"]) + } + } + + let captured = await fixture.backend.lastLookupRecords + #expect(captured?.recordNames == ["note-1", "note-2"]) + #expect(captured?.database == .private) + } + + @Test("POST /api/records/lookup returns 401 without a captured auth token") + internal func recordsLookupRequiresAuth() async throws { + let fixture = Self.makeFixture(authenticated: false) + let app = Application(router: try fixture.server.makeRouter()) + + try await app.test(.router) { client in + try await client.execute( + uri: "/api/records/lookup", + method: .post, + headers: [.contentType: "application/json"], + body: ByteBuffer(string: #"{"recordNames":["note-1"]}"#) + ) { response in + #expect(response.status == .unauthorized) + } + } + } + + @Test("POST /api/records/lookup surfaces a backend failure as an error") + internal func recordsLookupPropagatesBackendError() async throws { + // `webLookupRecords` is all-or-nothing: any per-record failure (e.g. + // CloudKit NOT_FOUND) throws rather than returning partial rows, so a + // backend error must surface as 500 — not a 200 with fewer records. + let fixture = Self.makeFixture(authenticated: true) + await fixture.backend.failNext(message: "record not found") + let app = Application(router: try fixture.server.makeRouter()) + + try await app.test(.router) { client in + try await client.execute( + uri: "/api/records/lookup", + method: .post, + headers: [.contentType: "application/json"], + body: ByteBuffer(string: #"{"recordNames":["missing"]}"#) + ) { response in + #expect(response.status == .internalServerError) + } + } + } + + @Test("POST /api/records/changes forwards zone and sync token to backend") + internal func recordsChangesForwards() async throws { + let fixture = Self.makeFixture(authenticated: true) + let app = Application(router: try fixture.server.makeRouter()) + let jsonBody = """ + {"database":"private","zoneName":"Notes","syncToken":"token-xyz"} + """ + + try await app.test(.router) { client in + try await client.execute( + uri: "/api/records/changes", + method: .post, + headers: [.contentType: "application/json"], + body: ByteBuffer(string: jsonBody) + ) { response in + #expect(response.status == .ok) + let payload = try JSONDecoder().decode( + RecordChangesPayload.self, + from: Data(response.body.readableBytesView) + ) + #expect(payload.syncToken == "stub-record-sync-token") + #expect(payload.moreComing == false) + #expect(payload.records.first?.recordName == "changed-1") + } + } + + let captured = await fixture.backend.lastRecordChanges + #expect(captured?.zoneName == "Notes") + #expect(captured?.syncToken == "token-xyz") + #expect(captured?.database == .private) + } + + @Test("POST /api/records/changes returns 401 without a captured auth token") + internal func recordsChangesRequiresAuth() async throws { + let fixture = Self.makeFixture(authenticated: false) + let app = Application(router: try fixture.server.makeRouter()) + + try await app.test(.router) { client in + try await client.execute( + uri: "/api/records/changes", + method: .post, + headers: [.contentType: "application/json"], + body: ByteBuffer(string: #"{"zoneName":"Notes"}"#) + ) { response in + #expect(response.status == .unauthorized) + } + } + } + } +#endif diff --git a/Examples/MistDemo/Tests/MistDemoTests/Server/WebServerTests+Users.swift b/Examples/MistDemo/Tests/MistDemoTests/Server/WebServerTests+Users.swift new file mode 100644 index 00000000..86b84c38 --- /dev/null +++ b/Examples/MistDemo/Tests/MistDemoTests/Server/WebServerTests+Users.swift @@ -0,0 +1,142 @@ +// +// WebServerTests+Users.swift +// MistDemoTests +// +// Created by Leo Dion. +// Copyright © 2026 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +#if canImport(Hummingbird) + internal import Foundation + internal import HTTPTypes + internal import Hummingbird + internal import HummingbirdTesting + internal import MistKit + internal import Testing + + @testable import MistDemoKit + + extension WebServerTests { + // UserInfo is Encodable-only, so decode the response shape locally. + private struct CallerPayload: Decodable { + struct User: Decodable { + let userRecordName: String + let emailAddress: String? + } + let user: User + } + + private struct UsersPayload: Decodable { + let users: [UserIdentity] + } + + @Test("GET /api/users/caller forwards to the backend") + internal func usersCallerForwards() async throws { + let fixture = Self.makeFixture(authenticated: true) + let app = Application(router: try fixture.server.makeRouter()) + + try await app.test(.router) { client in + try await client.execute( + uri: "/api/users/caller", + method: .get + ) { response in + #expect(response.status == .ok) + let payload = try JSONDecoder().decode( + CallerPayload.self, + from: Data(response.body.readableBytesView) + ) + #expect(payload.user.userRecordName == "stub-caller") + #expect(payload.user.emailAddress == "stub@example.com") + } + } + + let didFetch = await fixture.backend.didFetchCaller + #expect(didFetch) + } + + @Test("GET /api/users/caller returns 401 without a captured auth token") + internal func usersCallerRequiresAuth() async throws { + let fixture = Self.makeFixture(authenticated: false) + let app = Application(router: try fixture.server.makeRouter()) + + try await app.test(.router) { client in + try await client.execute( + uri: "/api/users/caller", + method: .get + ) { response in + #expect(response.status == .unauthorized) + } + } + } + + @Test( + "POST /api/users/discover forwards emails and record names to the backend" + ) + internal func usersDiscoverForwards() async throws { + let fixture = Self.makeFixture(authenticated: true) + let app = Application(router: try fixture.server.makeRouter()) + let jsonBody = """ + {"emails":["a@example.com","b@example.com"],\ + "userRecordNames":["_user-1"]} + """ + + try await app.test(.router) { client in + try await client.execute( + uri: "/api/users/discover", + method: .post, + headers: [.contentType: "application/json"], + body: ByteBuffer(string: jsonBody) + ) { response in + #expect(response.status == .ok) + let payload = try JSONDecoder().decode( + UsersPayload.self, + from: Data(response.body.readableBytesView) + ) + #expect(payload.users.count == 3) + } + } + + let captured = await fixture.backend.lastDiscoverUsers + #expect(captured?.emails == ["a@example.com", "b@example.com"]) + #expect(captured?.userRecordNames == ["_user-1"]) + } + + @Test("POST /api/users/discover returns 401 without a captured auth token") + internal func usersDiscoverRequiresAuth() async throws { + let fixture = Self.makeFixture(authenticated: false) + let app = Application(router: try fixture.server.makeRouter()) + + try await app.test(.router) { client in + try await client.execute( + uri: "/api/users/discover", + method: .post, + headers: [.contentType: "application/json"], + body: ByteBuffer(string: #"{"emails":["a@example.com"]}"#) + ) { response in + #expect(response.status == .unauthorized) + } + } + } + } +#endif diff --git a/Examples/MistDemo/Tests/MistDemoTests/Server/WebServerTests+ZoneReads.swift b/Examples/MistDemo/Tests/MistDemoTests/Server/WebServerTests+ZoneReads.swift new file mode 100644 index 00000000..1f7fa0d3 --- /dev/null +++ b/Examples/MistDemo/Tests/MistDemoTests/Server/WebServerTests+ZoneReads.swift @@ -0,0 +1,182 @@ +// +// WebServerTests+ZoneReads.swift +// MistDemoTests +// +// Created by Leo Dion. +// Copyright © 2026 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +#if canImport(Hummingbird) + internal import Foundation + internal import HTTPTypes + internal import Hummingbird + internal import HummingbirdTesting + internal import MistKit + internal import Testing + + @testable import MistDemoKit + + extension WebServerTests { + private struct ZonesPayload: Decodable { + let zones: [ZoneInfo] + } + + private struct ZoneChangesPayload: Decodable { + let zones: [ZoneInfo] + let syncToken: String? + let moreComing: Bool + } + + @Test("POST /api/zones/list forwards the database to the backend") + internal func zonesListForwards() async throws { + let fixture = Self.makeFixture(authenticated: true) + let app = Application(router: try fixture.server.makeRouter()) + + try await app.test(.router) { client in + try await client.execute( + uri: "/api/zones/list", + method: .post, + headers: [.contentType: "application/json"], + body: ByteBuffer(string: #"{"database":"private"}"#) + ) { response in + #expect(response.status == .ok) + let payload = try JSONDecoder().decode( + ZonesPayload.self, + from: Data(response.body.readableBytesView) + ) + #expect(payload.zones.first?.zoneName == "_defaultZone") + } + } + + let captured = await fixture.backend.lastListZones + #expect(captured?.database == .private) + } + + @Test("POST /api/zones/list returns 401 without a captured auth token") + internal func zonesListRequiresAuth() async throws { + let fixture = Self.makeFixture(authenticated: false) + let app = Application(router: try fixture.server.makeRouter()) + + try await app.test(.router) { client in + try await client.execute( + uri: "/api/zones/list", + method: .post, + headers: [.contentType: "application/json"], + body: ByteBuffer(string: #"{"database":"private"}"#) + ) { response in + #expect(response.status == .unauthorized) + } + } + } + + @Test("POST /api/zones/lookup forwards zone names to the backend") + internal func zonesLookupForwards() async throws { + let fixture = Self.makeFixture(authenticated: true) + let app = Application(router: try fixture.server.makeRouter()) + let jsonBody = #"{"database":"private","zoneNames":["Articles","Archive"]}"# + + try await app.test(.router) { client in + try await client.execute( + uri: "/api/zones/lookup", + method: .post, + headers: [.contentType: "application/json"], + body: ByteBuffer(string: jsonBody) + ) { response in + #expect(response.status == .ok) + let payload = try JSONDecoder().decode( + ZonesPayload.self, + from: Data(response.body.readableBytesView) + ) + #expect(payload.zones.map(\.zoneName) == ["Articles", "Archive"]) + } + } + + let captured = await fixture.backend.lastLookupZones + #expect(captured?.zoneNames == ["Articles", "Archive"]) + #expect(captured?.database == .private) + } + + @Test("POST /api/zones/lookup returns 401 without a captured auth token") + internal func zonesLookupRequiresAuth() async throws { + let fixture = Self.makeFixture(authenticated: false) + let app = Application(router: try fixture.server.makeRouter()) + + try await app.test(.router) { client in + try await client.execute( + uri: "/api/zones/lookup", + method: .post, + headers: [.contentType: "application/json"], + body: ByteBuffer(string: #"{"zoneNames":["Z"]}"#) + ) { response in + #expect(response.status == .unauthorized) + } + } + } + + @Test("POST /api/zones/changes forwards the sync token to the backend") + internal func zonesChangesForwards() async throws { + let fixture = Self.makeFixture(authenticated: true) + let app = Application(router: try fixture.server.makeRouter()) + let jsonBody = #"{"database":"private","syncToken":"token-abc"}"# + + try await app.test(.router) { client in + try await client.execute( + uri: "/api/zones/changes", + method: .post, + headers: [.contentType: "application/json"], + body: ByteBuffer(string: jsonBody) + ) { response in + #expect(response.status == .ok) + let payload = try JSONDecoder().decode( + ZoneChangesPayload.self, + from: Data(response.body.readableBytesView) + ) + #expect(payload.syncToken == "stub-zone-sync-token") + #expect(payload.moreComing == false) + } + } + + let captured = await fixture.backend.lastZoneChanges + #expect(captured?.syncToken == "token-abc") + #expect(captured?.database == .private) + } + + @Test("POST /api/zones/changes returns 401 without a captured auth token") + internal func zonesChangesRequiresAuth() async throws { + let fixture = Self.makeFixture(authenticated: false) + let app = Application(router: try fixture.server.makeRouter()) + + try await app.test(.router) { client in + try await client.execute( + uri: "/api/zones/changes", + method: .post, + headers: [.contentType: "application/json"], + body: ByteBuffer(string: #"{"syncToken":"t"}"#) + ) { response in + #expect(response.status == .unauthorized) + } + } + } + } +#endif From d8281e75714e7f215aef53bd1ca1d4522288f8b6 Mon Sep 17 00:00:00 2001 From: leogdion Date: Fri, 29 May 2026 09:58:46 -0400 Subject: [PATCH 30/35] Sync docs & markdown for the 1.0.0-beta.2 release (#400) --- CLAUDE.md | 22 ++++++++ Examples/CelestraCloud/CHANGELOG.md | 2 +- README.md | 82 +++++++++++++++++++++++++---- ReleaseNotes.md | 29 ++++++++++ 4 files changed, 123 insertions(+), 12 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 633b8fb8..294bdae1 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -461,6 +461,28 @@ For detailed schema workflows and integration: - **AI Schema Workflow** (`Examples/CelestraCloud/.claude/AI_SCHEMA_WORKFLOW.md`) - Comprehensive guide for understanding, designing, modifying, and validating CloudKit schemas with text-based tools - **Quick Reference** (`Examples/SCHEMA_QUICK_REFERENCE.md`) - One-page cheat sheet with syntax, patterns, cktool commands, and troubleshooting +## Examples + +The `Examples/` directory contains working applications that dogfood MistKit-under-development (see also the README "Examples" list). These are MistKit-dev test beds, not end-user deployment templates. + +### BushelCloud — the canonical MistKit pattern demonstration + +`Examples/BushelCloud/` is the most complete reference implementation of MistKit's core patterns — the backend that syncs macOS restore images, Xcode, and Swift versions for the [Bushel app](https://getbushel.app): + +- **Server-to-Server authentication** — loading an ECDSA `.pem` key and wiring `ServerToServerAuthManager` into `CloudKitService` (`Sources/BushelCloudKit/CloudKit/BushelCloudKitService.swift`, `PEMValidator.swift`). +- **Batch / chunked record operations** — working within CloudKit's 200-operations-per-request limit and aggregating results across batches (`CloudKit/SyncEngine.swift`, `CloudKit/BushelCloudKitService.swift`). +- **Multi-source data integration** — fetching and deduplicating from many upstream APIs (`DataSources/` — IPSW, MESU, AppleDB, XcodeReleases, SwiftVersion, …; `DataSourcePipeline+Deduplication.swift`). +- **CloudKit reference usage** — creating and resolving reference fields between record types (`DataSources/DataSourcePipeline+ReferenceResolution.swift`, `Extensions/XcodeVersionRecord+CloudKit.swift`). +- **Cross-platform logging** — swift-log with MistKit's subsystem organization (`Configuration/BushelConfiguration.swift`). + +### CelestraCloud — query filtering, sorting & web etiquette + +`Examples/CelestraCloud/` is a command-line RSS reader (backend for the [Celestra app](https://celestr.app)) demonstrating MistKit's `QueryFilter`/`QuerySort` APIs, GUID-based duplicate detection, and respectful HTTP client patterns. See its own `CLAUDE.md`. + +### MistDemo — interactive auth & endpoint demo + +`Examples/MistDemo/` is a CLI + App + Web demo exercising the beta.2 endpoint surface with web-auth token capture. See the project-level "MistDemo Commands" section above. + ## Import Conventions Every `import` statement must carry an explicit access modifier — `internal import X` or `public import X`. Bare `import X` is forbidden. Default to `internal`; use `public import` only when the module's types appear in this file's `public` API (e.g. `public import HTTPTypes` where `HTTPRequest` is part of a `public` signature). diff --git a/Examples/CelestraCloud/CHANGELOG.md b/Examples/CelestraCloud/CHANGELOG.md index 484bbf72..66700b48 100644 --- a/Examples/CelestraCloud/CHANGELOG.md +++ b/Examples/CelestraCloud/CHANGELOG.md @@ -70,7 +70,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Technical Details - **Platform**: macOS 26+ (Swift 6.2) - **Concurrency**: Full Swift 6 concurrency support with strict checking -- **Dependencies**: MistKit 1.0.0-alpha.3, SyndiKit 0.6.1, ArgumentParser, swift-log +- **Dependencies**: MistKit 1.0.0-beta.2, SyndiKit 0.6.1, ArgumentParser, swift-log - **CloudKit**: Public database with Feed and Article record types - **Schema**: Text-based .ckdb schema with cktool deployment diff --git a/README.md b/README.md index af82de56..12fdd2c4 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,7 @@ Add MistKit to your `Package.swift`: ```swift dependencies: [ - .package(url: "https://github.com/brightdigit/MistKit.git", from: "1.0.0-beta.1") + .package(url: "https://github.com/brightdigit/MistKit.git", from: "1.0.0-beta.2") ] ``` @@ -290,6 +290,62 @@ do { ### Advanced Usage +#### More Operations + +Beyond querying and CRUD, MistKit covers zones, subscriptions, push tokens, and +asset re-referencing. Every call takes an explicit `database:`. + +```swift +// Zones +let zone = try await service.createZone( + zoneName: "Notes", + database: .private +) +try await service.deleteZone(zoneName: "Notes", database: .private) + +// Subscriptions +let subs = try await service.listSubscriptions(database: .private) +let one = try await service.lookupSubscriptions(ids: ["sub-1"], database: .private) +// Create/update/delete via service.modifySubscriptions(_:database:) +// (takes [SubscriptionOperation], returns [SubscriptionResult]). + +// APNs push tokens +let token = try await service.createAPNsToken( + environment: .development, + database: .private +) +try await service.registerAPNsToken( + token.apnsToken, + environment: .development, + database: .private +) + +// Re-reference existing CDN assets without re-uploading bytes +let assets = try await service.rereferenceAssets( + [(recordName: "rec-1", fieldName: "photo")], + database: .private +) +``` + +#### Auto-Chunking Conveniences + +CloudKit caps batch requests at 200 items. `lookupAllRecords` and the +`lookupInfos:` form of `discoverAllUserIdentities` split oversized inputs into +≤`maxRecordsPerRequest` (200) batches automatically and concatenate the results +in input order — no manual chunking required. + +```swift +let records = try await service.lookupAllRecords( + recordNames: thousandsOfNames, // chunked into 200-item requests + database: .private +) + +let identities = try await service.discoverAllUserIdentities( + lookupInfos: manyLookupInfos, + batchSize: 200 +) +``` + #### HTTP Transport Non-WASI platforms default to `URLSessionTransport` — no transport plumbing is @@ -424,23 +480,27 @@ MistKit is released under the MIT License. See [LICENSE](LICENSE) for details. - [x] CI updates for May 2026 ([#277](https://github.com/brightdigit/MistKit/pull/277)) ✅ - [x] Fail lint job when any command fails ([#303](https://github.com/brightdigit/MistKit/pull/303)) ✅ -### v1.0.0-alpha.X +### v1.0.0-beta.2 + +- [x] [Referencing Existing Assets (assets/rereference)](https://github.com/brightdigit/MistKit/issues/31) ✅ +- [x] [Modifying Zones (zones/modify)](https://github.com/brightdigit/MistKit/issues/45) ✅ +- [x] [Fetching Subscriptions (subscriptions/list)](https://github.com/brightdigit/MistKit/issues/49) ✅ +- [x] [Fetching Subscriptions by Identifier (subscriptions/lookup)](https://github.com/brightdigit/MistKit/issues/50) ✅ +- [x] [Modifying Subscriptions (subscriptions/modify)](https://github.com/brightdigit/MistKit/issues/51) ✅ +- [x] [Creating APNs Tokens (tokens/create)](https://github.com/brightdigit/MistKit/issues/52) ✅ +- [x] [Registering Tokens (tokens/register)](https://github.com/brightdigit/MistKit/issues/53) ✅ +- [x] [Fetching Users by Email (users/lookup/email)](https://github.com/brightdigit/MistKit/issues/34) ✅ *(Apple-deprecated — prefer `discoverAllUserIdentities`)* +- [x] [Fetching Users by Record Name (users/lookup/id)](https://github.com/brightdigit/MistKit/issues/35) ✅ *(Apple-deprecated — prefer `discoverAllUserIdentities`)* +- [x] Auto-chunking conveniences for batch operations ([#389](https://github.com/brightdigit/MistKit/pull/389)) ✅ + +### Backlog / Post-beta - [ ] [Discovering All User Identities (GET users/discover)](https://github.com/brightdigit/MistKit/issues/28) -- [ ] [Referencing Existing Assets (assets/rereference)](https://github.com/brightdigit/MistKit/issues/31) - [ ] [Fetching Contacts (users/lookup/contacts)](https://github.com/brightdigit/MistKit/issues/33) -- [ ] [Fetching Users by Email (users/lookup/email)](https://github.com/brightdigit/MistKit/issues/34) -- [ ] [Fetching Users by Record Name (users/lookup/id)](https://github.com/brightdigit/MistKit/issues/35) - [ ] [Fetching Record Information (records/resolve)](https://github.com/brightdigit/MistKit/issues/41) - [ ] [Accepting Share Records (records/accept)](https://github.com/brightdigit/MistKit/issues/42) - [ ] [Fetching Database Changes (changes/database)](https://github.com/brightdigit/MistKit/issues/46) -- [ ] [Modifying Zones (zones/modify)](https://github.com/brightdigit/MistKit/issues/45) - [ ] [Fetching Record Zone Changes (changes/zone)](https://github.com/brightdigit/MistKit/issues/47) -- [ ] [Fetching Subscriptions (subscriptions/list)](https://github.com/brightdigit/MistKit/issues/49) -- [ ] [Fetching Subscriptions by Identifier (subscriptions/lookup)](https://github.com/brightdigit/MistKit/issues/50) -- [ ] [Modifying Subscriptions (subscriptions/modify)](https://github.com/brightdigit/MistKit/issues/51) -- [ ] [Creating APNs Tokens (tokens/create)](https://github.com/brightdigit/MistKit/issues/52) -- [ ] [Registering Tokens (tokens/register)](https://github.com/brightdigit/MistKit/issues/53) - [ ] [Feature: Add custom CloudKit zone support for queries](https://github.com/brightdigit/MistKit/issues/146) ### v1.0.0 diff --git a/ReleaseNotes.md b/ReleaseNotes.md index c6087bf4..7807bb29 100644 --- a/ReleaseNotes.md +++ b/ReleaseNotes.md @@ -1,3 +1,32 @@ +## 1.0.0-beta.2 + +### Subscriptions & Push Notifications +* Push Notifications & Subscriptions epic — `listSubscriptions`, `lookupSubscriptions`, `modifySubscriptions`, `createAPNsToken`, `registerAPNsToken` by @leogdion in https://github.com/brightdigit/MistKit/pull/381 + +### Zones +* Zone API: `createZone`, `deleteZone`, `fetchAllZoneChanges` by @leogdion in https://github.com/brightdigit/MistKit/pull/367 +* `list-zones`, `modify-zones`, discover, and validate by @leogdion in https://github.com/brightdigit/MistKit/pull/368 + +### Assets +* Implement `assets/rereference` endpoint and API by @leogdion in https://github.com/brightdigit/MistKit/pull/393 + +### Batch Conveniences +* Auto-chunking conveniences for batch operations (`lookupAllRecords`, `discoverAllUserIdentities(lookupInfos:batchSize:)`) by @leogdion in https://github.com/brightdigit/MistKit/pull/389 + +### Correctness & Safety +* Tag and validate ambiguous `FieldValue` scalar types (`TIMESTAMP`, `BYTES`, `DOUBLE`) by @leogdion in https://github.com/brightdigit/MistKit/pull/377 +* Make response→domain conversion failures loud; add `RecordResult` by @leogdion in https://github.com/brightdigit/MistKit/pull/372 +* Pre-1.0.0 correctness & safety hardening by @leogdion in https://github.com/brightdigit/MistKit/pull/357 +* Style & error audit: explicit import access + scoped flake gates by @leogdion in https://github.com/brightdigit/MistKit/pull/363 + +### Tooling, MistDemo & Docs +* Scaffold MistDemo (CLI + App + Web) for v1.0.0-beta.2 endpoints by @leogdion in https://github.com/brightdigit/MistKit/pull/371 +* Wire landed MistKit endpoints into the MistDemo web app by @leogdion in https://github.com/brightdigit/MistKit/pull/396 +* `setup-mistkit`: pin to resolved revision by @leogdion in https://github.com/brightdigit/MistKit/pull/380 +* Enable MistDemo integration workflow on `claude/**` branches by @leogdion in https://github.com/brightdigit/MistKit/pull/374 + +**Full Changelog**: https://github.com/brightdigit/MistKit/compare/1.0.0-beta.1...1.0.0-beta.2 + ## 1.0.0-beta.1 ### Querying & Sync From a151095861247441af119d4ae5d34b8c3c472ccd Mon Sep 17 00:00:00 2001 From: Leo Dion Date: Fri, 29 May 2026 10:49:24 -0400 Subject: [PATCH 31/35] Add Mermaid architecture diagrams to BushelCloud README (#140) Add data-flow, component-architecture, MistKit-integration, and CloudKit schema-relationship diagrams to the Architecture and CloudKit Schema sections, replacing the prior ASCII art. Co-Authored-By: Claude Opus 4.7 (1M context) --- Examples/BushelCloud/README.md | 60 ++++++++++++++++++++++++++++------ 1 file changed, 50 insertions(+), 10 deletions(-) diff --git a/Examples/BushelCloud/README.md b/Examples/BushelCloud/README.md index f74476d8..b902fcb3 100644 --- a/Examples/BushelCloud/README.md +++ b/Examples/BushelCloud/README.md @@ -40,6 +40,21 @@ In Apple's virtualization framework, **restore images** are used to boot virtual ## Architecture +### Data Flow + +The sync pipeline moves version data from external APIs into CloudKit in three phases — **fetch** (parallel API calls), **transform** (deduplicate and resolve references), and **upload** (batched writes): + +```mermaid +graph LR + A[External APIs
IPSW · AppleDB · MESU
XcodeReleases · Swift.org · VirtualBuddy] --> B[Fetchers] + B --> C[DataSourcePipeline] + C --> D[Deduplication
and Merge] + D --> E[RecordBuilder] + E --> F[BushelCloudKitService] + F --> G[MistKit
CloudKitService] + G --> H[(CloudKit
Web Services)] +``` + ### Data Sources The demo integrates with multiple data sources to gather comprehensive version information: @@ -97,6 +112,33 @@ BushelCloud/ └── ExportCommand.swift ``` +The modules interact as follows — the CLI drives `SyncEngine`, which fans out to the data pipeline for fetching and to the service layer for uploading: + +```mermaid +graph TD + CLI[BushelCloudCLI
sync · export · clear · list · status] --> SE[SyncEngine] + SE --> DSP[DataSourcePipeline] + SE --> SVC[BushelCloudKitService] + DSP --> FET[Fetchers
IPSW · AppleDB · MESU
XcodeReleases · SwiftVersion · VirtualBuddy] + SVC --> MK[MistKit
CloudKitService] + MK --> CK[(CloudKit API)] +``` + +### MistKit Integration Pattern + +BushelCloud authenticates with **Server-to-Server** credentials (no signed-in iCloud user). An ECDSA P-256 `.pem` key and key ID build a `ServerToServerAuthManager`, which is injected into `CloudKitService`. The database scope is chosen per call (`.public(.prefers(.serverToServer))`), and writes are chunked to CloudKit's 200-operations-per-request limit: + +```mermaid +graph TD + PEM[ECDSA P-256
.pem private key] --> SAM[ServerToServerAuthManager] + KID[CLOUDKIT_KEY_ID] --> SAM + SAM --> CKS[CloudKitService] + CFG[Container ID
+ environment] --> CKS + CKS --> OPS{Record operations} + OPS -->|chunk into ≤200| BATCH[modifyRecords
per batch] + BATCH --> API[(CloudKit Web Services
public DB · S2S-signed)] +``` + ### BushelKit Integration BushelCloud uses [BushelKit](https://github.com/brightdigit/BushelKit) as its modular foundation, providing: @@ -315,19 +357,17 @@ For Xcode setup and debugging instructions, see the "Xcode Development Setup" se ## CloudKit Schema -The demo uses three record types with relationships: +The demo uses three related record types (plus a standalone `DataSourceMetadata`). `XcodeVersion` holds `CKReference` fields to the macOS restore image it requires and the Swift compiler it bundles: -```text -SwiftVersion - ↑ - | (reference) - | -RestoreImage ← XcodeVersion - ↑ ↑ - | (reference) | - |______________| +```mermaid +graph TD + XV[XcodeVersion] -->|minimumMacOS · CKReference| RI[RestoreImage] + XV -->|swiftVersion · CKReference| SV[SwiftVersion] + DSM[DataSourceMetadata
no references] ``` +Because references must resolve at write time, the referenced records (`RestoreImage`, `SwiftVersion`) are uploaded before `XcodeVersion`. + ### Record Relationships - **XcodeVersion → RestoreImage**: Links Xcode to minimum macOS version required From 503ad2b6566916a9aa3c45b46b66bceb64df548d Mon Sep 17 00:00:00 2001 From: Leo Dion Date: Fri, 29 May 2026 13:25:11 -0400 Subject: [PATCH 32/35] git subrepo commit (merge) Examples/CelestraCloud subrepo: subdir: "Examples/CelestraCloud" merged: "cfab860" upstream: origin: "git@github.com:brightdigit/CelestraCloud.git" branch: "mistkit" commit: "a0c2488" git-subrepo: version: "0.4.9" origin: "https://github.com/Homebrew/brew" commit: "6f293daa9f" --- Examples/CelestraCloud/.gitrepo | 2 +- Examples/CelestraCloud/Scripts/lint.sh | 3 +- .../Services/FeedUpdateResult.swift | 25 --- .../Protocols/CloudKitRecordOperating.swift | 6 +- .../Services/CelestraError.swift | 161 ++++++++++-------- .../CloudKitConfigurationTests.swift | 2 +- .../UpdateCommandConfigurationTests.swift | 2 +- .../CelestraErrorTests+Description.swift | 2 +- ...elestraErrorTests+RecoverySuggestion.swift | 2 +- .../Errors/CelestraErrorTests.swift | 2 +- .../Errors/CloudKitConversionErrorTests.swift | 2 +- .../ArticleConversion+FromCloudKit.swift | 29 ++++ .../ArticleConversion+ToCloudKit.swift | 29 ++++ .../Extensions/ArticleConversion.swift | 29 ++++ .../FeedConversion+FromCloudKit.swift | 29 ++++ .../Extensions/FeedConversion+RoundTrip.swift | 29 ++++ .../FeedConversion+ToCloudKit.swift | 29 ++++ .../Extensions/FeedConversion.swift | 29 ++++ .../Mocks/MockCloudKitRecordOperator.swift | 12 +- .../Models/BatchOperationResultTests.swift | 29 ++++ .../ArticleCategorizer+Advanced.swift | 2 +- .../Services/ArticleCategorizer+Basic.swift | 2 +- .../Services/ArticleCategorizer.swift | 29 ++++ .../ArticleCloudKitService+Mutations.swift | 14 +- .../ArticleCloudKitService+Query.swift | 17 +- .../Services/ArticleCloudKitService.swift | 29 ++++ .../Services/FeedCloudKitService+CRUD.swift | 2 +- .../Services/FeedCloudKitService+Query.swift | 22 +-- .../Services/FeedCloudKitService.swift | 29 ++++ .../Services/FeedMetadataBuilder+Error.swift | 28 +-- .../FeedMetadataBuilder+NotModified.swift | 15 +- .../FeedMetadataBuilder+Success.swift | 2 +- .../Services/FeedMetadataBuilder.swift | 29 ++++ 33 files changed, 462 insertions(+), 211 deletions(-) diff --git a/Examples/CelestraCloud/.gitrepo b/Examples/CelestraCloud/.gitrepo index 4076631e..9091c8b9 100644 --- a/Examples/CelestraCloud/.gitrepo +++ b/Examples/CelestraCloud/.gitrepo @@ -6,7 +6,7 @@ [subrepo] remote = git@github.com:brightdigit/CelestraCloud.git branch = mistkit - commit = 910b9fb60e224ece2e87c6057e3f554d60545a4e + commit = a0c24888cc5a9d97450588d45dab2e9b0b67161e parent = f7673d3e2823ecb79e44cbea98340351714fa9f5 method = merge cmdver = 0.4.9 diff --git a/Examples/CelestraCloud/Scripts/lint.sh b/Examples/CelestraCloud/Scripts/lint.sh index dc840547..985de1e2 100755 --- a/Examples/CelestraCloud/Scripts/lint.sh +++ b/Examples/CelestraCloud/Scripts/lint.sh @@ -58,7 +58,8 @@ if [ -z "$FORMAT_ONLY" ]; then run_command swift build --build-tests fi -$PACKAGE_DIR/Scripts/header.sh -d $PACKAGE_DIR/Sources -c "Leo Dion" -o "BrightDigit" -p "CelestraCloud" +run_command $PACKAGE_DIR/Scripts/header.sh -d "$PACKAGE_DIR/Sources" -c "Leo Dion" -o "BrightDigit" -p "CelestraCloud" +run_command $PACKAGE_DIR/Scripts/header.sh -d "$PACKAGE_DIR/Tests" -c "Leo Dion" -o "BrightDigit" -p "CelestraCloud" # Generated files now automatically include ignore directives via OpenAPI generator configuration diff --git a/Examples/CelestraCloud/Sources/CelestraCloud/Services/FeedUpdateResult.swift b/Examples/CelestraCloud/Sources/CelestraCloud/Services/FeedUpdateResult.swift index a0143868..b08f7e30 100644 --- a/Examples/CelestraCloud/Sources/CelestraCloud/Services/FeedUpdateResult.swift +++ b/Examples/CelestraCloud/Sources/CelestraCloud/Services/FeedUpdateResult.swift @@ -35,29 +35,4 @@ internal enum FeedUpdateResult: Sendable, Equatable { case notModified case skipped(reason: String) case error(message: String) - - // MARK: - Subtypes - - internal enum SimpleStatus { - case success - case notModified - case skipped - case error - } - - // MARK: - Properties - - /// Simple status for backward compatibility - internal var simpleStatus: SimpleStatus { - switch self { - case .success: - return .success - case .notModified: - return .notModified - case .skipped: - return .skipped - case .error: - return .error - } - } } diff --git a/Examples/CelestraCloud/Sources/CelestraCloudKit/Protocols/CloudKitRecordOperating.swift b/Examples/CelestraCloud/Sources/CelestraCloudKit/Protocols/CloudKitRecordOperating.swift index 01afb30f..083a907b 100644 --- a/Examples/CelestraCloud/Sources/CelestraCloudKit/Protocols/CloudKitRecordOperating.swift +++ b/Examples/CelestraCloud/Sources/CelestraCloudKit/Protocols/CloudKitRecordOperating.swift @@ -99,7 +99,8 @@ extension CloudKitService: CloudKitRecordOperating { return records } - /// Satisfy CloudKitRecordOperating's `queryRecords` (no database param) by forwarding to the public-database overload. + /// Satisfy CloudKitRecordOperating's `queryRecords` (no database param) + /// by forwarding to the public-database overload. public func queryRecords( recordType: String, filters: [QueryFilter]?, @@ -119,7 +120,8 @@ extension CloudKitService: CloudKitRecordOperating { return result.records } - /// Satisfy CloudKitRecordOperating's `queryAllRecords` (no database param) by forwarding to the public-database overload. + /// Satisfy CloudKitRecordOperating's `queryAllRecords` (no database param) + /// by forwarding to the public-database overload. public func queryAllRecords( recordType: String, filters: [QueryFilter]?, diff --git a/Examples/CelestraCloud/Sources/CelestraCloudKit/Services/CelestraError.swift b/Examples/CelestraCloud/Sources/CelestraCloudKit/Services/CelestraError.swift index cee7f485..edbcd542 100644 --- a/Examples/CelestraCloud/Sources/CelestraCloudKit/Services/CelestraError.swift +++ b/Examples/CelestraCloud/Sources/CelestraCloudKit/Services/CelestraError.swift @@ -62,67 +62,77 @@ public enum CelestraError: LocalizedError { /// Invalid record name case invalidRecordName(String) + // MARK: - Lookup Tables + + // The tables below are keyed by `caseID` (see the discriminator at the bottom + // of this file) so that cases with associated values — which can't be written + // as case-literal keys — participate alongside the payload-free cases. + + /// Cases that are retriable on their own. `cloudKitError` is decided + /// separately, delegating to the wrapped `CloudKitError`. + private static let retriableCaseIDs: Set = [ + .rssFetchFailed, + .networkUnavailable, + ] + + /// Error descriptions for cases whose text doesn't depend on associated values. + private static let staticDescriptions: [CaseID: String] = [ + .quotaExceeded: "CloudKit quota exceeded. Please try again later.", + .networkUnavailable: "Network unavailable. Check your connection.", + .permissionDenied: "Permission denied for CloudKit operation.", + ] + + /// Recovery suggestions. Cases absent here have no suggestion (`nil`). + private static let recoverySuggestions: [CaseID: String] = [ + .rssFetchFailed: "Verify the feed URL is accessible and try again.", + .invalidFeedData: "Verify the feed URL returns valid RSS/Atom data.", + .quotaExceeded: "Wait a few minutes for CloudKit quota to reset, then try again.", + .networkUnavailable: "Check your internet connection and try again.", + .permissionDenied: "Check your CloudKit permissions and API token configuration.", + ] + // MARK: - Retriability /// Determines if this error can be retried public var isRetriable: Bool { - switch self { - case .cloudKitError(let ckError): + if case .cloudKitError(let ckError) = self { return isCloudKitErrorRetriable(ckError) - case .rssFetchFailed, .networkUnavailable: - return true - case .quotaExceeded, .invalidFeedData, .batchOperationFailed, - .permissionDenied, .recordNotFound, .cloudKitOperationFailed, .invalidRecordName: - return false } + return Self.retriableCaseIDs.contains(caseID) } // MARK: - LocalizedError Conformance /// Localized error description public var errorDescription: String? { - switch self { - case .cloudKitError(let error): - return "CloudKit operation failed: \(error.localizedDescription)" - case .rssFetchFailed(let url, let error): - return "Failed to fetch RSS feed from \(url.absoluteString): \(error.localizedDescription)" - case .invalidFeedData(let reason): - return "Invalid feed data: \(reason)" - case .batchOperationFailed(let errors): - return "Batch operation failed with \(errors.count) error(s)" - case .quotaExceeded: - return "CloudKit quota exceeded. Please try again later." - case .networkUnavailable: - return "Network unavailable. Check your connection." - case .permissionDenied: - return "Permission denied for CloudKit operation." - case .recordNotFound(let recordName): - return "Record not found: \(recordName)" - case .cloudKitOperationFailed(let message): - return "CloudKit operation failed: \(message)" - case .invalidRecordName(let message): - return "Invalid record name: \(message)" + // Cases without associated values get their text from the lookup table; + // the rest interpolate their payloads. + guard let staticDescription = Self.staticDescriptions[caseID] else { + switch self { + case .cloudKitError(let error): + return "CloudKit operation failed: \(error.localizedDescription)" + case .rssFetchFailed(let url, let error): + return "Failed to fetch RSS feed from \(url.absoluteString): \(error.localizedDescription)" + case .invalidFeedData(let reason): + return "Invalid feed data: \(reason)" + case .batchOperationFailed(let errors): + return "Batch operation failed with \(errors.count) error(s)" + case .recordNotFound(let recordName): + return "Record not found: \(recordName)" + case .cloudKitOperationFailed(let message): + return "CloudKit operation failed: \(message)" + case .invalidRecordName(let message): + return "Invalid record name: \(message)" + default: + assertionFailure("Missing `errorDescription` for case: \(self).") + return nil + } } + return staticDescription } /// Suggested recovery action for the error - public var recoverySuggestion: String? { - switch self { - case .quotaExceeded: - return "Wait a few minutes for CloudKit quota to reset, then try again." - case .networkUnavailable: - return "Check your internet connection and try again." - case .rssFetchFailed: - return "Verify the feed URL is accessible and try again." - case .permissionDenied: - return "Check your CloudKit permissions and API token configuration." - case .invalidFeedData: - return "Verify the feed URL returns valid RSS/Atom data." - case .cloudKitError, .batchOperationFailed, .recordNotFound, - .cloudKitOperationFailed, .invalidRecordName: - return nil - } - } + public var recoverySuggestion: String? { Self.recoverySuggestions[caseID] } // MARK: - CloudKit Error Classification @@ -135,35 +145,42 @@ public enum CelestraError: LocalizedError { // Retry on server errors (5xx) and rate limiting (429) // Don't retry on client errors (4xx) except 429 return statusCode >= 500 || statusCode == 429 - case .invalidResponse, .underlyingError: - // Network-related errors are retriable - return true - case .networkError: - // Network errors are retriable + // Network-related/transient errors are retriable + case .invalidResponse, .underlyingError, .networkError: return true - case .decodingError: - // Decoding errors are not retriable (data format issue) - return false - case .unsupportedOperationType, .paginationLimitExceeded, .zonePaginationLimitExceeded: - // Programmer/configuration issues — not retriable - return false - case .conversionFailed, .recordOperationFailed, .incompleteResponse: - // Response could not be mapped or was incomplete, or a per-record - // operation failed — not retriable - return false - case .subscriptionOperationFailed, .subscriptionLikelyDuplicate: - // Subscription operation failed or was a duplicate — not retriable - return false - case .missingCredentials, .invalidPrivateKey: - // Credential/configuration issues — not retriable - return false - case .badRequest, .atomicFailure: - // Server-side malformed-request / atomic-batch failures — not retriable - return false - case .quotaExceeded: - // Could be size-limit (not retriable) or storage-quota exhaustion - // (also not retriable until the user frees space). Either way, no. + + // Everything else (decoding, configuration, credential, malformed-request, + // and quota errors) is not retriable. + default: return false } } } + +// MARK: - Case Identity + +extension CelestraError { + /// Payload-free mirror of `CelestraError`'s cases. Used as the key into the + /// lookup tables above so that cases with associated values can be classified + /// without being written as case literals. + private enum CaseID { + case cloudKitError, rssFetchFailed, invalidFeedData, batchOperationFailed, + quotaExceeded, networkUnavailable, permissionDenied, recordNotFound, + cloudKitOperationFailed, invalidRecordName + } + + private var caseID: CaseID { + switch self { + case .cloudKitError: .cloudKitError + case .rssFetchFailed: .rssFetchFailed + case .invalidFeedData: .invalidFeedData + case .batchOperationFailed: .batchOperationFailed + case .quotaExceeded: .quotaExceeded + case .networkUnavailable: .networkUnavailable + case .permissionDenied: .permissionDenied + case .recordNotFound: .recordNotFound + case .cloudKitOperationFailed: .cloudKitOperationFailed + case .invalidRecordName: .invalidRecordName + } + } +} diff --git a/Examples/CelestraCloud/Tests/CelestraCloudTests/Configuration/CloudKitConfigurationTests.swift b/Examples/CelestraCloud/Tests/CelestraCloudTests/Configuration/CloudKitConfigurationTests.swift index d3825535..80d18002 100644 --- a/Examples/CelestraCloud/Tests/CelestraCloudTests/Configuration/CloudKitConfigurationTests.swift +++ b/Examples/CelestraCloud/Tests/CelestraCloudTests/Configuration/CloudKitConfigurationTests.swift @@ -3,7 +3,7 @@ // CelestraCloud // // Created by Leo Dion. -// Copyright © 2025 BrightDigit. +// Copyright © 2026 BrightDigit. // // Permission is hereby granted, free of charge, to any person // obtaining a copy of this software and associated documentation diff --git a/Examples/CelestraCloud/Tests/CelestraCloudTests/Configuration/UpdateCommandConfigurationTests.swift b/Examples/CelestraCloud/Tests/CelestraCloudTests/Configuration/UpdateCommandConfigurationTests.swift index 9cc6188e..39a5903c 100644 --- a/Examples/CelestraCloud/Tests/CelestraCloudTests/Configuration/UpdateCommandConfigurationTests.swift +++ b/Examples/CelestraCloud/Tests/CelestraCloudTests/Configuration/UpdateCommandConfigurationTests.swift @@ -3,7 +3,7 @@ // CelestraCloud // // Created by Leo Dion. -// Copyright © 2025 BrightDigit. +// Copyright © 2026 BrightDigit. // // Permission is hereby granted, free of charge, to any person // obtaining a copy of this software and associated documentation diff --git a/Examples/CelestraCloud/Tests/CelestraCloudTests/Errors/CelestraErrorTests+Description.swift b/Examples/CelestraCloud/Tests/CelestraCloudTests/Errors/CelestraErrorTests+Description.swift index 5701e264..f73e2b39 100644 --- a/Examples/CelestraCloud/Tests/CelestraCloudTests/Errors/CelestraErrorTests+Description.swift +++ b/Examples/CelestraCloud/Tests/CelestraCloudTests/Errors/CelestraErrorTests+Description.swift @@ -3,7 +3,7 @@ // CelestraCloud // // Created by Leo Dion. -// Copyright © 2025 BrightDigit. +// Copyright © 2026 BrightDigit. // // Permission is hereby granted, free of charge, to any person // obtaining a copy of this software and associated documentation diff --git a/Examples/CelestraCloud/Tests/CelestraCloudTests/Errors/CelestraErrorTests+RecoverySuggestion.swift b/Examples/CelestraCloud/Tests/CelestraCloudTests/Errors/CelestraErrorTests+RecoverySuggestion.swift index cc968d6c..031edc5c 100644 --- a/Examples/CelestraCloud/Tests/CelestraCloudTests/Errors/CelestraErrorTests+RecoverySuggestion.swift +++ b/Examples/CelestraCloud/Tests/CelestraCloudTests/Errors/CelestraErrorTests+RecoverySuggestion.swift @@ -3,7 +3,7 @@ // CelestraCloud // // Created by Leo Dion. -// Copyright © 2025 BrightDigit. +// Copyright © 2026 BrightDigit. // // Permission is hereby granted, free of charge, to any person // obtaining a copy of this software and associated documentation diff --git a/Examples/CelestraCloud/Tests/CelestraCloudTests/Errors/CelestraErrorTests.swift b/Examples/CelestraCloud/Tests/CelestraCloudTests/Errors/CelestraErrorTests.swift index 0274d8d0..b1a2b7a7 100644 --- a/Examples/CelestraCloud/Tests/CelestraCloudTests/Errors/CelestraErrorTests.swift +++ b/Examples/CelestraCloud/Tests/CelestraCloudTests/Errors/CelestraErrorTests.swift @@ -3,7 +3,7 @@ // CelestraCloud // // Created by Leo Dion. -// Copyright © 2025 BrightDigit. +// Copyright © 2026 BrightDigit. // // Permission is hereby granted, free of charge, to any person // obtaining a copy of this software and associated documentation diff --git a/Examples/CelestraCloud/Tests/CelestraCloudTests/Errors/CloudKitConversionErrorTests.swift b/Examples/CelestraCloud/Tests/CelestraCloudTests/Errors/CloudKitConversionErrorTests.swift index 114df916..7baa72e0 100644 --- a/Examples/CelestraCloud/Tests/CelestraCloudTests/Errors/CloudKitConversionErrorTests.swift +++ b/Examples/CelestraCloud/Tests/CelestraCloudTests/Errors/CloudKitConversionErrorTests.swift @@ -3,7 +3,7 @@ // CelestraCloud // // Created by Leo Dion. -// Copyright © 2025 BrightDigit. +// Copyright © 2026 BrightDigit. // // Permission is hereby granted, free of charge, to any person // obtaining a copy of this software and associated documentation diff --git a/Examples/CelestraCloud/Tests/CelestraCloudTests/Extensions/ArticleConversion+FromCloudKit.swift b/Examples/CelestraCloud/Tests/CelestraCloudTests/Extensions/ArticleConversion+FromCloudKit.swift index a08aa956..b628db72 100644 --- a/Examples/CelestraCloud/Tests/CelestraCloudTests/Extensions/ArticleConversion+FromCloudKit.swift +++ b/Examples/CelestraCloud/Tests/CelestraCloudTests/Extensions/ArticleConversion+FromCloudKit.swift @@ -1,3 +1,32 @@ +// +// ArticleConversion+FromCloudKit.swift +// CelestraCloud +// +// Created by Leo Dion. +// Copyright © 2026 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + internal import CelestraKit internal import Foundation internal import MistKit diff --git a/Examples/CelestraCloud/Tests/CelestraCloudTests/Extensions/ArticleConversion+ToCloudKit.swift b/Examples/CelestraCloud/Tests/CelestraCloudTests/Extensions/ArticleConversion+ToCloudKit.swift index 1a091f2d..1c0d3561 100644 --- a/Examples/CelestraCloud/Tests/CelestraCloudTests/Extensions/ArticleConversion+ToCloudKit.swift +++ b/Examples/CelestraCloud/Tests/CelestraCloudTests/Extensions/ArticleConversion+ToCloudKit.swift @@ -1,3 +1,32 @@ +// +// ArticleConversion+ToCloudKit.swift +// CelestraCloud +// +// Created by Leo Dion. +// Copyright © 2026 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + internal import CelestraKit internal import Foundation internal import MistKit diff --git a/Examples/CelestraCloud/Tests/CelestraCloudTests/Extensions/ArticleConversion.swift b/Examples/CelestraCloud/Tests/CelestraCloudTests/Extensions/ArticleConversion.swift index 08e4a2d2..e1c49436 100644 --- a/Examples/CelestraCloud/Tests/CelestraCloudTests/Extensions/ArticleConversion.swift +++ b/Examples/CelestraCloud/Tests/CelestraCloudTests/Extensions/ArticleConversion.swift @@ -1,2 +1,31 @@ +// +// ArticleConversion.swift +// CelestraCloud +// +// Created by Leo Dion. +// Copyright © 2026 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + /// Namespace for Article conversion tests internal enum ArticleConversion {} diff --git a/Examples/CelestraCloud/Tests/CelestraCloudTests/Extensions/FeedConversion+FromCloudKit.swift b/Examples/CelestraCloud/Tests/CelestraCloudTests/Extensions/FeedConversion+FromCloudKit.swift index f6284f97..6069dcea 100644 --- a/Examples/CelestraCloud/Tests/CelestraCloudTests/Extensions/FeedConversion+FromCloudKit.swift +++ b/Examples/CelestraCloud/Tests/CelestraCloudTests/Extensions/FeedConversion+FromCloudKit.swift @@ -1,3 +1,32 @@ +// +// FeedConversion+FromCloudKit.swift +// CelestraCloud +// +// Created by Leo Dion. +// Copyright © 2026 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + internal import CelestraKit internal import Foundation internal import MistKit diff --git a/Examples/CelestraCloud/Tests/CelestraCloudTests/Extensions/FeedConversion+RoundTrip.swift b/Examples/CelestraCloud/Tests/CelestraCloudTests/Extensions/FeedConversion+RoundTrip.swift index d3bdaa01..0a0e92b5 100644 --- a/Examples/CelestraCloud/Tests/CelestraCloudTests/Extensions/FeedConversion+RoundTrip.swift +++ b/Examples/CelestraCloud/Tests/CelestraCloudTests/Extensions/FeedConversion+RoundTrip.swift @@ -1,3 +1,32 @@ +// +// FeedConversion+RoundTrip.swift +// CelestraCloud +// +// Created by Leo Dion. +// Copyright © 2026 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + internal import CelestraKit internal import Foundation internal import MistKit diff --git a/Examples/CelestraCloud/Tests/CelestraCloudTests/Extensions/FeedConversion+ToCloudKit.swift b/Examples/CelestraCloud/Tests/CelestraCloudTests/Extensions/FeedConversion+ToCloudKit.swift index be3919f8..1ba5245a 100644 --- a/Examples/CelestraCloud/Tests/CelestraCloudTests/Extensions/FeedConversion+ToCloudKit.swift +++ b/Examples/CelestraCloud/Tests/CelestraCloudTests/Extensions/FeedConversion+ToCloudKit.swift @@ -1,3 +1,32 @@ +// +// FeedConversion+ToCloudKit.swift +// CelestraCloud +// +// Created by Leo Dion. +// Copyright © 2026 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + internal import CelestraKit internal import Foundation internal import MistKit diff --git a/Examples/CelestraCloud/Tests/CelestraCloudTests/Extensions/FeedConversion.swift b/Examples/CelestraCloud/Tests/CelestraCloudTests/Extensions/FeedConversion.swift index e2e69be8..54f7d2c2 100644 --- a/Examples/CelestraCloud/Tests/CelestraCloudTests/Extensions/FeedConversion.swift +++ b/Examples/CelestraCloud/Tests/CelestraCloudTests/Extensions/FeedConversion.swift @@ -1,2 +1,31 @@ +// +// FeedConversion.swift +// CelestraCloud +// +// Created by Leo Dion. +// Copyright © 2026 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + /// Namespace for Feed conversion tests internal enum FeedConversion {} diff --git a/Examples/CelestraCloud/Tests/CelestraCloudTests/Mocks/MockCloudKitRecordOperator.swift b/Examples/CelestraCloud/Tests/CelestraCloudTests/Mocks/MockCloudKitRecordOperator.swift index 63049388..636f2e73 100644 --- a/Examples/CelestraCloud/Tests/CelestraCloudTests/Mocks/MockCloudKitRecordOperator.swift +++ b/Examples/CelestraCloud/Tests/CelestraCloudTests/Mocks/MockCloudKitRecordOperator.swift @@ -3,7 +3,7 @@ // CelestraCloud // // Created by Leo Dion. -// Copyright © 2025 BrightDigit. +// Copyright © 2026 BrightDigit. // // Permission is hereby granted, free of charge, to any person // obtaining a copy of this software and associated documentation @@ -45,9 +45,7 @@ internal final class MockCloudKitRecordOperator: CloudKitRecordOperating, Sendab internal struct QueryCall: Sendable { internal let recordType: String internal let filters: [QueryFilter]? - internal let sortBy: [QuerySort]? internal let limit: Int? - internal let desiredKeys: [String]? } internal struct ModifyCall: Sendable { @@ -97,9 +95,7 @@ internal final class MockCloudKitRecordOperator: CloudKitRecordOperating, Sendab QueryCall( recordType: recordType, filters: filters, - sortBy: sortBy, - limit: limit, - desiredKeys: desiredKeys + limit: limit ) ) return state.queryRecordsResult @@ -130,9 +126,7 @@ internal final class MockCloudKitRecordOperator: CloudKitRecordOperating, Sendab QueryCall( recordType: recordType, filters: filters, - sortBy: sortBy, - limit: pageSize, - desiredKeys: desiredKeys + limit: pageSize ) ) return state.queryRecordsResult diff --git a/Examples/CelestraCloud/Tests/CelestraCloudTests/Models/BatchOperationResultTests.swift b/Examples/CelestraCloud/Tests/CelestraCloudTests/Models/BatchOperationResultTests.swift index 3f70016c..e8176827 100644 --- a/Examples/CelestraCloud/Tests/CelestraCloudTests/Models/BatchOperationResultTests.swift +++ b/Examples/CelestraCloud/Tests/CelestraCloudTests/Models/BatchOperationResultTests.swift @@ -1,3 +1,32 @@ +// +// BatchOperationResultTests.swift +// CelestraCloud +// +// Created by Leo Dion. +// Copyright © 2026 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + internal import CelestraKit internal import Foundation internal import MistKit diff --git a/Examples/CelestraCloud/Tests/CelestraCloudTests/Services/ArticleCategorizer+Advanced.swift b/Examples/CelestraCloud/Tests/CelestraCloudTests/Services/ArticleCategorizer+Advanced.swift index 830f95f4..a16e5261 100644 --- a/Examples/CelestraCloud/Tests/CelestraCloudTests/Services/ArticleCategorizer+Advanced.swift +++ b/Examples/CelestraCloud/Tests/CelestraCloudTests/Services/ArticleCategorizer+Advanced.swift @@ -3,7 +3,7 @@ // CelestraCloud // // Created by Leo Dion. -// Copyright © 2025 BrightDigit. +// Copyright © 2026 BrightDigit. // // Permission is hereby granted, free of charge, to any person // obtaining a copy of this software and associated documentation diff --git a/Examples/CelestraCloud/Tests/CelestraCloudTests/Services/ArticleCategorizer+Basic.swift b/Examples/CelestraCloud/Tests/CelestraCloudTests/Services/ArticleCategorizer+Basic.swift index 415ad4bb..4d8d764e 100644 --- a/Examples/CelestraCloud/Tests/CelestraCloudTests/Services/ArticleCategorizer+Basic.swift +++ b/Examples/CelestraCloud/Tests/CelestraCloudTests/Services/ArticleCategorizer+Basic.swift @@ -3,7 +3,7 @@ // CelestraCloud // // Created by Leo Dion. -// Copyright © 2025 BrightDigit. +// Copyright © 2026 BrightDigit. // // Permission is hereby granted, free of charge, to any person // obtaining a copy of this software and associated documentation diff --git a/Examples/CelestraCloud/Tests/CelestraCloudTests/Services/ArticleCategorizer.swift b/Examples/CelestraCloud/Tests/CelestraCloudTests/Services/ArticleCategorizer.swift index f02ff0ee..3a5d9433 100644 --- a/Examples/CelestraCloud/Tests/CelestraCloudTests/Services/ArticleCategorizer.swift +++ b/Examples/CelestraCloud/Tests/CelestraCloudTests/Services/ArticleCategorizer.swift @@ -1,2 +1,31 @@ +// +// ArticleCategorizer.swift +// CelestraCloud +// +// Created by Leo Dion. +// Copyright © 2026 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + /// Namespace for ArticleCategorizer tests internal enum ArticleCategorizer {} diff --git a/Examples/CelestraCloud/Tests/CelestraCloudTests/Services/ArticleCloudKitService+Mutations.swift b/Examples/CelestraCloud/Tests/CelestraCloudTests/Services/ArticleCloudKitService+Mutations.swift index 0fabb3da..f364a9dd 100644 --- a/Examples/CelestraCloud/Tests/CelestraCloudTests/Services/ArticleCloudKitService+Mutations.swift +++ b/Examples/CelestraCloud/Tests/CelestraCloudTests/Services/ArticleCloudKitService+Mutations.swift @@ -3,7 +3,7 @@ // CelestraCloud // // Created by Leo Dion. -// Copyright © 2025 BrightDigit. +// Copyright © 2026 BrightDigit. // // Permission is hereby granted, free of charge, to any person // obtaining a copy of this software and associated documentation @@ -66,18 +66,6 @@ extension ArticleCloudKitService { ) } - private func createArticleRecordFields(guid: String = "test-guid") -> [String: FieldValue] { - [ - "feedRecordName": .string("feed-123"), - "guid": .string(guid), - "title": .string("Test Article"), - "url": .string("https://example.com/article"), - "fetchedTimestamp": .date(Date(timeIntervalSince1970: 1_000_000)), - "expiresTimestamp": .date(Date(timeIntervalSince1970: 1_000_000 + 30 * 24 * 60 * 60)), - "contentHash": .string("abc123"), - ] - } - // MARK: - createArticles Tests @Test("createArticles returns empty result for empty input") diff --git a/Examples/CelestraCloud/Tests/CelestraCloudTests/Services/ArticleCloudKitService+Query.swift b/Examples/CelestraCloud/Tests/CelestraCloudTests/Services/ArticleCloudKitService+Query.swift index 3e080ee6..130a1c16 100644 --- a/Examples/CelestraCloud/Tests/CelestraCloudTests/Services/ArticleCloudKitService+Query.swift +++ b/Examples/CelestraCloud/Tests/CelestraCloudTests/Services/ArticleCloudKitService+Query.swift @@ -3,7 +3,7 @@ // CelestraCloud // // Created by Leo Dion. -// Copyright © 2025 BrightDigit. +// Copyright © 2026 BrightDigit. // // Permission is hereby granted, free of charge, to any person // obtaining a copy of this software and associated documentation @@ -51,21 +51,6 @@ extension ArticleCloudKitService { ) } - private func createTestArticle( - recordName: String? = nil, - guid: String = "test-guid" - ) -> Article { - Article( - recordName: recordName, - feedRecordName: "feed-123", - guid: guid, - title: "Test Article", - url: "https://example.com/article", - fetchedAt: Date(timeIntervalSince1970: 1_000_000), - ttlDays: 30 - ) - } - private func createArticleRecordFields(guid: String = "test-guid") -> [String: FieldValue] { [ "feedRecordName": .string("feed-123"), diff --git a/Examples/CelestraCloud/Tests/CelestraCloudTests/Services/ArticleCloudKitService.swift b/Examples/CelestraCloud/Tests/CelestraCloudTests/Services/ArticleCloudKitService.swift index 32714368..22bc144b 100644 --- a/Examples/CelestraCloud/Tests/CelestraCloudTests/Services/ArticleCloudKitService.swift +++ b/Examples/CelestraCloud/Tests/CelestraCloudTests/Services/ArticleCloudKitService.swift @@ -1,2 +1,31 @@ +// +// ArticleCloudKitService.swift +// CelestraCloud +// +// Created by Leo Dion. +// Copyright © 2026 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + /// Namespace for ArticleCloudKitService tests internal enum ArticleCloudKitService {} diff --git a/Examples/CelestraCloud/Tests/CelestraCloudTests/Services/FeedCloudKitService+CRUD.swift b/Examples/CelestraCloud/Tests/CelestraCloudTests/Services/FeedCloudKitService+CRUD.swift index 35af1533..081f90d7 100644 --- a/Examples/CelestraCloud/Tests/CelestraCloudTests/Services/FeedCloudKitService+CRUD.swift +++ b/Examples/CelestraCloud/Tests/CelestraCloudTests/Services/FeedCloudKitService+CRUD.swift @@ -3,7 +3,7 @@ // CelestraCloud // // Created by Leo Dion. -// Copyright © 2025 BrightDigit. +// Copyright © 2026 BrightDigit. // // Permission is hereby granted, free of charge, to any person // obtaining a copy of this software and associated documentation diff --git a/Examples/CelestraCloud/Tests/CelestraCloudTests/Services/FeedCloudKitService+Query.swift b/Examples/CelestraCloud/Tests/CelestraCloudTests/Services/FeedCloudKitService+Query.swift index e4f598a7..99741511 100644 --- a/Examples/CelestraCloud/Tests/CelestraCloudTests/Services/FeedCloudKitService+Query.swift +++ b/Examples/CelestraCloud/Tests/CelestraCloudTests/Services/FeedCloudKitService+Query.swift @@ -3,7 +3,7 @@ // CelestraCloud // // Created by Leo Dion. -// Copyright © 2025 BrightDigit. +// Copyright © 2026 BrightDigit. // // Permission is hereby granted, free of charge, to any person // obtaining a copy of this software and associated documentation @@ -51,26 +51,6 @@ extension FeedCloudKitService { ) } - private func createTestFeed() -> Feed { - Feed( - recordName: nil, - feedURL: "https://example.com/feed.xml", - title: "Test Feed", - description: "A test feed", - isFeatured: false, - isVerified: true, - subscriberCount: 100, - totalAttempts: 5, - successfulAttempts: 4, - lastAttempted: Date(timeIntervalSince1970: 1_000_000), - isActive: true, - etag: "etag-123", - lastModified: "Mon, 01 Jan 2024 00:00:00 GMT", - failureCount: 1, - minUpdateInterval: 3_600 - ) - } - // MARK: - queryFeeds Tests @Test("queryFeeds returns feeds from query results") diff --git a/Examples/CelestraCloud/Tests/CelestraCloudTests/Services/FeedCloudKitService.swift b/Examples/CelestraCloud/Tests/CelestraCloudTests/Services/FeedCloudKitService.swift index 5afd86bc..15fd25a3 100644 --- a/Examples/CelestraCloud/Tests/CelestraCloudTests/Services/FeedCloudKitService.swift +++ b/Examples/CelestraCloud/Tests/CelestraCloudTests/Services/FeedCloudKitService.swift @@ -1,2 +1,31 @@ +// +// FeedCloudKitService.swift +// CelestraCloud +// +// Created by Leo Dion. +// Copyright © 2026 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + /// Namespace for FeedCloudKitService tests internal enum FeedCloudKitService {} diff --git a/Examples/CelestraCloud/Tests/CelestraCloudTests/Services/FeedMetadataBuilder+Error.swift b/Examples/CelestraCloud/Tests/CelestraCloudTests/Services/FeedMetadataBuilder+Error.swift index 8b2889a8..b169e088 100644 --- a/Examples/CelestraCloud/Tests/CelestraCloudTests/Services/FeedMetadataBuilder+Error.swift +++ b/Examples/CelestraCloud/Tests/CelestraCloudTests/Services/FeedMetadataBuilder+Error.swift @@ -3,7 +3,7 @@ // CelestraCloud // // Created by Leo Dion. -// Copyright © 2025 BrightDigit. +// Copyright © 2026 BrightDigit. // // Permission is hereby granted, free of charge, to any person // obtaining a copy of this software and associated documentation @@ -62,32 +62,6 @@ extension FeedMetadataBuilder { ) } - private func createFeedData( - title: String = "New Feed Title", - description: String? = "New Feed Description", - minUpdateInterval: TimeInterval? = 7_200 - ) -> FeedData { - FeedData( - title: title, - description: description, - items: [], // Not used in metadata building - minUpdateInterval: minUpdateInterval - ) - } - - private func createFetchResponse( - feedData: FeedData? = nil, - etag: String? = "new-etag", - lastModified: String? = "Tue, 02 Jan 2024 00:00:00 GMT" - ) -> FetchResponse { - FetchResponse( - feedData: feedData, - lastModified: lastModified, - etag: etag, - wasModified: feedData != nil - ) - } - // MARK: - Error Metadata Tests @Test("Error metadata preserves all feed data") diff --git a/Examples/CelestraCloud/Tests/CelestraCloudTests/Services/FeedMetadataBuilder+NotModified.swift b/Examples/CelestraCloud/Tests/CelestraCloudTests/Services/FeedMetadataBuilder+NotModified.swift index 65263546..3c49fba8 100644 --- a/Examples/CelestraCloud/Tests/CelestraCloudTests/Services/FeedMetadataBuilder+NotModified.swift +++ b/Examples/CelestraCloud/Tests/CelestraCloudTests/Services/FeedMetadataBuilder+NotModified.swift @@ -3,7 +3,7 @@ // CelestraCloud // // Created by Leo Dion. -// Copyright © 2025 BrightDigit. +// Copyright © 2026 BrightDigit. // // Permission is hereby granted, free of charge, to any person // obtaining a copy of this software and associated documentation @@ -62,19 +62,6 @@ extension FeedMetadataBuilder { ) } - private func createFeedData( - title: String = "New Feed Title", - description: String? = "New Feed Description", - minUpdateInterval: TimeInterval? = 7_200 - ) -> FeedData { - FeedData( - title: title, - description: description, - items: [], // Not used in metadata building - minUpdateInterval: minUpdateInterval - ) - } - private func createFetchResponse( feedData: FeedData? = nil, etag: String? = "new-etag", diff --git a/Examples/CelestraCloud/Tests/CelestraCloudTests/Services/FeedMetadataBuilder+Success.swift b/Examples/CelestraCloud/Tests/CelestraCloudTests/Services/FeedMetadataBuilder+Success.swift index 3af551ea..f075b3b4 100644 --- a/Examples/CelestraCloud/Tests/CelestraCloudTests/Services/FeedMetadataBuilder+Success.swift +++ b/Examples/CelestraCloud/Tests/CelestraCloudTests/Services/FeedMetadataBuilder+Success.swift @@ -3,7 +3,7 @@ // CelestraCloud // // Created by Leo Dion. -// Copyright © 2025 BrightDigit. +// Copyright © 2026 BrightDigit. // // Permission is hereby granted, free of charge, to any person // obtaining a copy of this software and associated documentation diff --git a/Examples/CelestraCloud/Tests/CelestraCloudTests/Services/FeedMetadataBuilder.swift b/Examples/CelestraCloud/Tests/CelestraCloudTests/Services/FeedMetadataBuilder.swift index b0bbf0d9..b8e661b0 100644 --- a/Examples/CelestraCloud/Tests/CelestraCloudTests/Services/FeedMetadataBuilder.swift +++ b/Examples/CelestraCloud/Tests/CelestraCloudTests/Services/FeedMetadataBuilder.swift @@ -1,2 +1,31 @@ +// +// FeedMetadataBuilder.swift +// CelestraCloud +// +// Created by Leo Dion. +// Copyright © 2026 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + /// Namespace for FeedMetadataBuilder tests internal enum FeedMetadataBuilder {} From 83496aeb0c7f72f87bf4cdb6e4df4f36737cec56 Mon Sep 17 00:00:00 2001 From: Leo Dion Date: Fri, 29 May 2026 13:29:07 -0400 Subject: [PATCH 33/35] git subrepo push Examples/CelestraCloud Co-Authored-By: Claude Opus 4.8 (1M context) --- Examples/CelestraCloud/.gitrepo | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Examples/CelestraCloud/.gitrepo b/Examples/CelestraCloud/.gitrepo index 9091c8b9..8cc20d02 100644 --- a/Examples/CelestraCloud/.gitrepo +++ b/Examples/CelestraCloud/.gitrepo @@ -6,7 +6,7 @@ [subrepo] remote = git@github.com:brightdigit/CelestraCloud.git branch = mistkit - commit = a0c24888cc5a9d97450588d45dab2e9b0b67161e - parent = f7673d3e2823ecb79e44cbea98340351714fa9f5 + commit = 4c532d39cd0540b08899c09fe80c24b00a7fd1ce + parent = 503ad2b6566916a9aa3c45b46b66bceb64df548d method = merge cmdver = 0.4.9 From fe0a6ae37cc09970570aaa5f1eacef948a81b177 Mon Sep 17 00:00:00 2001 From: Leo Dion Date: Fri, 29 May 2026 14:05:56 -0400 Subject: [PATCH 34/35] Fix BushelCloud subrepo parent after squash-merge rewrote sync point The recorded parent 3b84901 (PR #379/#381 review commit) was squashed out of history, breaking git-subrepo's ancestor check. Repoint to the still-valid sync anchor 3452060 per git-subrepo's recovery suggestion. Co-Authored-By: Claude Opus 4.8 (1M context) --- Examples/BushelCloud/.gitrepo | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Examples/BushelCloud/.gitrepo b/Examples/BushelCloud/.gitrepo index 5724f840..245214eb 100644 --- a/Examples/BushelCloud/.gitrepo +++ b/Examples/BushelCloud/.gitrepo @@ -7,6 +7,6 @@ remote = git@github.com:brightdigit/BushelCloud.git branch = mistkit commit = 66bbe468d8a80c375b1a03c229f68a510eaf4f0a - parent = 3b84901cd775dfc0fb928ce4b4cc7e52c0badc64 + parent = 34520607e658f15d0159514ebbc9c36601c1b11a method = merge cmdver = 0.4.9 From b0b361a1d2d85d2d2134a957ae2f45b9a9f68b4c Mon Sep 17 00:00:00 2001 From: Leo Dion Date: Fri, 29 May 2026 14:09:59 -0400 Subject: [PATCH 35/35] git subrepo push Examples/BushelCloud subrepo: subdir: "Examples/BushelCloud" merged: "1ab45e4" upstream: origin: "git@github.com:brightdigit/BushelCloud.git" branch: "mistkit" commit: "1ab45e4" git-subrepo: version: "0.4.9" origin: "https://github.com/Homebrew/brew" commit: "6f293daa9f" --- Examples/BushelCloud/.gitrepo | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Examples/BushelCloud/.gitrepo b/Examples/BushelCloud/.gitrepo index 245214eb..d9c179c7 100644 --- a/Examples/BushelCloud/.gitrepo +++ b/Examples/BushelCloud/.gitrepo @@ -6,7 +6,7 @@ [subrepo] remote = git@github.com:brightdigit/BushelCloud.git branch = mistkit - commit = 66bbe468d8a80c375b1a03c229f68a510eaf4f0a - parent = 34520607e658f15d0159514ebbc9c36601c1b11a + commit = 1ab45e4fe8420bc52c3c27f3740b770e49e36b9b + parent = fe0a6ae37cc09970570aaa5f1eacef948a81b177 method = merge cmdver = 0.4.9