buildanything 1.7.1 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +3 -3
- package/.claude-plugin/plugin.json +9 -3
- package/CHANGELOG.md +112 -0
- package/README.md +2 -2
- package/agents/a11y-architect.md +166 -0
- package/agents/business-model.md +80 -29
- package/agents/code-architect.md +75 -0
- package/agents/code-reviewer.md +255 -0
- package/agents/code-simplifier.md +64 -0
- package/agents/design-brand-guardian.md +293 -53
- package/agents/design-critic.md +139 -0
- package/agents/design-inclusive-visuals-specialist.md +6 -19
- package/agents/design-ui-designer.md +335 -56
- package/agents/design-ux-architect.md +403 -55
- package/agents/design-ux-researcher.md +264 -49
- package/agents/engineering-ai-engineer.md +26 -36
- package/agents/engineering-backend-architect.md +185 -36
- package/agents/engineering-data-engineer.md +225 -43
- package/agents/engineering-devops-automator.md +227 -74
- package/agents/engineering-frontend-developer.md +210 -34
- package/agents/engineering-mobile-app-builder.md +6 -1
- package/agents/engineering-rapid-prototyper.md +30 -9
- package/agents/engineering-security-engineer.md +263 -61
- package/agents/engineering-senior-developer.md +128 -19
- package/agents/engineering-sre.md +84 -0
- package/agents/engineering-technical-writer.md +285 -41
- package/agents/feature-intel.md +110 -0
- package/agents/ios-app-review-guardian.md +66 -0
- package/agents/ios-foundation-models-specialist.md +64 -0
- package/agents/ios-storekit-specialist.md +59 -0
- package/agents/ios-swift-architect.md +129 -0
- package/agents/ios-swift-search.md +137 -0
- package/agents/ios-swift-ui-design.md +136 -0
- package/agents/marketing-app-store-optimizer.md +246 -64
- package/agents/planner.md +216 -0
- package/agents/pr-test-analyzer.md +63 -0
- package/agents/product-feedback-synthesizer.md +8 -2
- package/agents/refactor-cleaner.md +102 -0
- package/agents/security-reviewer.md +128 -0
- package/agents/silent-failure-hunter.md +54 -0
- package/agents/swift-build-resolver.md +119 -0
- package/agents/swift-reviewer.md +112 -0
- package/agents/tech-feasibility.md +21 -1
- package/agents/testing-api-tester.md +236 -59
- package/agents/testing-evidence-collector.md +26 -1
- package/agents/testing-performance-benchmarker.md +21 -1
- package/agents/testing-reality-checker.md +6 -1
- package/agents/visual-research.md +116 -0
- package/bin/adapters/cycle-counter-tool.ts +155 -0
- package/bin/adapters/scribe-tool.ts +71 -0
- package/bin/adapters/state-save-tool.ts +130 -0
- package/bin/adapters/write-lease-tool.ts +127 -0
- package/bin/buildanything-runtime.js +15 -0
- package/bin/buildanything-runtime.ts +328 -0
- package/bin/setup.js +83 -8
- package/commands/add-feature.md +2 -0
- package/commands/build.md +752 -332
- package/commands/fix.md +65 -0
- package/commands/self-check.md +121 -0
- package/commands/setup.md +114 -0
- package/commands/ux-review.md +63 -0
- package/commands/verify.md +69 -0
- package/docs/migration/agents.yaml +729 -0
- package/docs/migration/phase-graph.yaml +1088 -0
- package/docs/migration/sdk-host-compat.md +18 -0
- package/hooks/compile-writer-owner-cache.ts +171 -0
- package/hooks/hooks.json +36 -0
- package/hooks/pre-tool-use +19 -0
- package/hooks/pre-tool-use.ts +776 -0
- package/hooks/record-mode-transitions.ts +178 -0
- package/hooks/session-start +89 -2
- package/hooks/subagent-start +17 -0
- package/hooks/subagent-start.ts +471 -0
- package/hooks/subagent-stop +17 -0
- package/hooks/subagent-stop.ts +153 -0
- package/package.json +28 -5
- package/protocols/architecture-schema.md +171 -0
- package/protocols/build-fix.md +52 -0
- package/protocols/cleanup.md +54 -0
- package/protocols/decision-log.md +131 -0
- package/protocols/eval-harness.md +61 -0
- package/protocols/fake-data-detector.md +64 -0
- package/protocols/ios-context.md +234 -0
- package/protocols/ios-frameworks-map.md +323 -0
- package/protocols/ios-phase-branches.md +337 -0
- package/protocols/ios-preflight.md +27 -0
- package/protocols/launch-readiness.md +258 -0
- package/protocols/metric-loop.md +153 -0
- package/protocols/smoke-test.md +118 -0
- package/protocols/state-schema.json +388 -0
- package/protocols/state-schema.md +172 -0
- package/protocols/verify.md +127 -0
- package/protocols/visual-dna.md +185 -0
- package/protocols/web-phase-branches.md +351 -0
- package/skills/ios/_VENDORED.md +62 -0
- package/skills/ios/activitykit/LICENSE +131 -0
- package/skills/ios/activitykit/SKILL.md +505 -0
- package/skills/ios/activitykit/references/activitykit-patterns.md +868 -0
- package/skills/ios/app-intents/LICENSE +131 -0
- package/skills/ios/app-intents/SKILL.md +494 -0
- package/skills/ios/app-intents/references/appintents-advanced.md +1076 -0
- package/skills/ios/app-store-connect-metadata/SKILL.md +148 -0
- package/skills/ios/apple-on-device-ai/LICENSE +131 -0
- package/skills/ios/apple-on-device-ai/SKILL.md +505 -0
- package/skills/ios/apple-on-device-ai/references/coreml-conversion.md +425 -0
- package/skills/ios/apple-on-device-ai/references/coreml-optimization.md +344 -0
- package/skills/ios/apple-on-device-ai/references/foundation-models.md +508 -0
- package/skills/ios/apple-on-device-ai/references/mlx-swift.md +285 -0
- package/skills/ios/asc-privacy-manifest/SKILL.md +350 -0
- package/skills/ios/hig-components-content/SKILL.md +86 -0
- package/skills/ios/hig-components-content/references/activity-views.md +79 -0
- package/skills/ios/hig-components-content/references/charts.md +180 -0
- package/skills/ios/hig-components-content/references/collections.md +48 -0
- package/skills/ios/hig-components-content/references/color-wells.md +42 -0
- package/skills/ios/hig-components-content/references/image-views.md +82 -0
- package/skills/ios/hig-components-content/references/image-wells.md +34 -0
- package/skills/ios/hig-components-content/references/lockups.md +78 -0
- package/skills/ios/hig-components-content/references/web-views.md +36 -0
- package/skills/ios/hig-components-controls/SKILL.md +88 -0
- package/skills/ios/hig-components-controls/references/combo-boxes.md +40 -0
- package/skills/ios/hig-components-controls/references/controls.md +112 -0
- package/skills/ios/hig-components-controls/references/gauges.md +74 -0
- package/skills/ios/hig-components-controls/references/labels.md +92 -0
- package/skills/ios/hig-components-controls/references/pickers.md +128 -0
- package/skills/ios/hig-components-controls/references/rating-indicators.md +38 -0
- package/skills/ios/hig-components-controls/references/segmented-controls.md +94 -0
- package/skills/ios/hig-components-controls/references/sliders.md +92 -0
- package/skills/ios/hig-components-controls/references/steppers.md +40 -0
- package/skills/ios/hig-components-controls/references/text-fields.md +88 -0
- package/skills/ios/hig-components-controls/references/text-views.md +56 -0
- package/skills/ios/hig-components-controls/references/toggles.md +127 -0
- package/skills/ios/hig-components-controls/references/token-fields.md +48 -0
- package/skills/ios/hig-components-controls/references/virtual-keyboards.md +156 -0
- package/skills/ios/hig-components-dialogs/SKILL.md +76 -0
- package/skills/ios/hig-components-dialogs/references/action-sheets.md +74 -0
- package/skills/ios/hig-components-dialogs/references/alerts.md +158 -0
- package/skills/ios/hig-components-dialogs/references/digit-entry-views.md +32 -0
- package/skills/ios/hig-components-dialogs/references/popovers.md +81 -0
- package/skills/ios/hig-components-dialogs/references/sheets.md +157 -0
- package/skills/ios/hig-components-layout/SKILL.md +99 -0
- package/skills/ios/hig-components-layout/references/boxes.md +48 -0
- package/skills/ios/hig-components-layout/references/column-views.md +44 -0
- package/skills/ios/hig-components-layout/references/lists-and-tables.md +99 -0
- package/skills/ios/hig-components-layout/references/ornaments.md +56 -0
- package/skills/ios/hig-components-layout/references/outline-views.md +64 -0
- package/skills/ios/hig-components-layout/references/panels.md +75 -0
- package/skills/ios/hig-components-layout/references/scroll-views.md +123 -0
- package/skills/ios/hig-components-layout/references/sidebars.md +109 -0
- package/skills/ios/hig-components-layout/references/split-views.md +110 -0
- package/skills/ios/hig-components-layout/references/tab-bars.md +173 -0
- package/skills/ios/hig-components-layout/references/tab-views.md +68 -0
- package/skills/ios/hig-components-layout/references/windows.md +188 -0
- package/skills/ios/hig-components-menus/SKILL.md +81 -0
- package/skills/ios/hig-components-menus/references/action-button.md +61 -0
- package/skills/ios/hig-components-menus/references/buttons.md +261 -0
- package/skills/ios/hig-components-menus/references/context-menus.md +105 -0
- package/skills/ios/hig-components-menus/references/disclosure-controls.md +84 -0
- package/skills/ios/hig-components-menus/references/dock-menus.md +40 -0
- package/skills/ios/hig-components-menus/references/edit-menus.md +88 -0
- package/skills/ios/hig-components-menus/references/menus.md +171 -0
- package/skills/ios/hig-components-menus/references/pop-up-buttons.md +70 -0
- package/skills/ios/hig-components-menus/references/pull-down-buttons.md +77 -0
- package/skills/ios/hig-components-menus/references/the-menu-bar.md +303 -0
- package/skills/ios/hig-components-menus/references/toolbars.md +256 -0
- package/skills/ios/hig-components-search/SKILL.md +68 -0
- package/skills/ios/hig-components-search/references/page-controls.md +120 -0
- package/skills/ios/hig-components-search/references/path-controls.md +40 -0
- package/skills/ios/hig-components-search/references/search-fields.md +189 -0
- package/skills/ios/hig-components-status/SKILL.md +80 -0
- package/skills/ios/hig-components-status/references/activity-rings.md +105 -0
- package/skills/ios/hig-components-status/references/progress-indicators.md +116 -0
- package/skills/ios/hig-components-status/references/status-bars.md +38 -0
- package/skills/ios/hig-components-system/SKILL.md +88 -0
- package/skills/ios/hig-components-system/references/app-clips.md +387 -0
- package/skills/ios/hig-components-system/references/app-shortcuts.md +114 -0
- package/skills/ios/hig-components-system/references/complications.md +425 -0
- package/skills/ios/hig-components-system/references/home-screen-quick-actions.md +42 -0
- package/skills/ios/hig-components-system/references/live-activities.md +442 -0
- package/skills/ios/hig-components-system/references/notifications.md +153 -0
- package/skills/ios/hig-components-system/references/top-shelf.md +135 -0
- package/skills/ios/hig-components-system/references/watch-faces.md +40 -0
- package/skills/ios/hig-components-system/references/widgets.md +517 -0
- package/skills/ios/hig-foundations/SKILL.md +98 -0
- package/skills/ios/hig-foundations/references/accessibility.md +291 -0
- package/skills/ios/hig-foundations/references/app-icons.md +210 -0
- package/skills/ios/hig-foundations/references/branding.md +44 -0
- package/skills/ios/hig-foundations/references/color.md +274 -0
- package/skills/ios/hig-foundations/references/dark-mode.md +116 -0
- package/skills/ios/hig-foundations/references/icons.md +263 -0
- package/skills/ios/hig-foundations/references/images.md +176 -0
- package/skills/ios/hig-foundations/references/immersive-experiences.md +174 -0
- package/skills/ios/hig-foundations/references/inclusion.md +189 -0
- package/skills/ios/hig-foundations/references/layout.md +425 -0
- package/skills/ios/hig-foundations/references/materials.md +238 -0
- package/skills/ios/hig-foundations/references/motion.md +103 -0
- package/skills/ios/hig-foundations/references/privacy.md +231 -0
- package/skills/ios/hig-foundations/references/right-to-left.md +206 -0
- package/skills/ios/hig-foundations/references/sf-symbols.md +310 -0
- package/skills/ios/hig-foundations/references/spatial-layout.md +142 -0
- package/skills/ios/hig-foundations/references/typography.md +1146 -0
- package/skills/ios/hig-foundations/references/writing.md +91 -0
- package/skills/ios/hig-inputs/SKILL.md +94 -0
- package/skills/ios/hig-inputs/references/apple-pencil-and-scribble.md +148 -0
- package/skills/ios/hig-inputs/references/camera-control.md +107 -0
- package/skills/ios/hig-inputs/references/digital-crown.md +83 -0
- package/skills/ios/hig-inputs/references/eyes.md +120 -0
- package/skills/ios/hig-inputs/references/focus-and-selection.md +120 -0
- package/skills/ios/hig-inputs/references/game-controls.md +156 -0
- package/skills/ios/hig-inputs/references/gestures.md +208 -0
- package/skills/ios/hig-inputs/references/gyro-and-accelerometer.md +40 -0
- package/skills/ios/hig-inputs/references/keyboards.md +234 -0
- package/skills/ios/hig-inputs/references/nearby-interactions.md +70 -0
- package/skills/ios/hig-inputs/references/pointing-devices.md +237 -0
- package/skills/ios/hig-inputs/references/remotes.md +67 -0
- package/skills/ios/hig-inputs/references/spatial-interactions.md +70 -0
- package/skills/ios/hig-patterns/SKILL.md +104 -0
- package/skills/ios/hig-patterns/references/charting-data.md +81 -0
- package/skills/ios/hig-patterns/references/collaboration-and-sharing.md +86 -0
- package/skills/ios/hig-patterns/references/drag-and-drop.md +134 -0
- package/skills/ios/hig-patterns/references/entering-data.md +69 -0
- package/skills/ios/hig-patterns/references/feedback.md +67 -0
- package/skills/ios/hig-patterns/references/file-management.md +135 -0
- package/skills/ios/hig-patterns/references/going-full-screen.md +79 -0
- package/skills/ios/hig-patterns/references/launching.md +81 -0
- package/skills/ios/hig-patterns/references/live-viewing-apps.md +79 -0
- package/skills/ios/hig-patterns/references/loading.md +59 -0
- package/skills/ios/hig-patterns/references/managing-accounts.md +107 -0
- package/skills/ios/hig-patterns/references/managing-notifications.md +99 -0
- package/skills/ios/hig-patterns/references/modality.md +82 -0
- package/skills/ios/hig-patterns/references/multitasking.md +131 -0
- package/skills/ios/hig-patterns/references/offering-help.md +117 -0
- package/skills/ios/hig-patterns/references/onboarding.md +69 -0
- package/skills/ios/hig-patterns/references/playing-audio.md +124 -0
- package/skills/ios/hig-patterns/references/playing-haptics.md +280 -0
- package/skills/ios/hig-patterns/references/playing-video.md +180 -0
- package/skills/ios/hig-patterns/references/printing.md +50 -0
- package/skills/ios/hig-patterns/references/ratings-and-reviews.md +48 -0
- package/skills/ios/hig-patterns/references/searching.md +70 -0
- package/skills/ios/hig-patterns/references/settings.md +84 -0
- package/skills/ios/hig-patterns/references/undo-and-redo.md +58 -0
- package/skills/ios/hig-patterns/references/workouts.md +76 -0
- package/skills/ios/hig-platforms/SKILL.md +84 -0
- package/skills/ios/hig-platforms/references/designing-for-games.md +159 -0
- package/skills/ios/hig-platforms/references/designing-for-ios.md +66 -0
- package/skills/ios/hig-platforms/references/designing-for-ipados.md +64 -0
- package/skills/ios/hig-platforms/references/designing-for-macos.md +70 -0
- package/skills/ios/hig-platforms/references/designing-for-tvos.md +68 -0
- package/skills/ios/hig-platforms/references/designing-for-visionos.md +85 -0
- package/skills/ios/hig-platforms/references/designing-for-watchos.md +74 -0
- package/skills/ios/hig-project-context/SKILL.md +133 -0
- package/skills/ios/hig-technologies/SKILL.md +107 -0
- package/skills/ios/hig-technologies/references/airplay.md +125 -0
- package/skills/ios/hig-technologies/references/always-on.md +62 -0
- package/skills/ios/hig-technologies/references/apple-pay.md +441 -0
- package/skills/ios/hig-technologies/references/augmented-reality.md +247 -0
- package/skills/ios/hig-technologies/references/carekit.md +224 -0
- package/skills/ios/hig-technologies/references/carplay.md +119 -0
- package/skills/ios/hig-technologies/references/game-center.md +343 -0
- package/skills/ios/hig-technologies/references/generative-ai.md +110 -0
- package/skills/ios/hig-technologies/references/healthkit.md +120 -0
- package/skills/ios/hig-technologies/references/homekit.md +343 -0
- package/skills/ios/hig-technologies/references/icloud.md +52 -0
- package/skills/ios/hig-technologies/references/id-verifier.md +73 -0
- package/skills/ios/hig-technologies/references/imessage-apps-and-stickers.md +105 -0
- package/skills/ios/hig-technologies/references/in-app-purchase.md +263 -0
- package/skills/ios/hig-technologies/references/live-photos.md +54 -0
- package/skills/ios/hig-technologies/references/mac-catalyst.md +216 -0
- package/skills/ios/hig-technologies/references/machine-learning.md +394 -0
- package/skills/ios/hig-technologies/references/maps.md +221 -0
- package/skills/ios/hig-technologies/references/nfc.md +51 -0
- package/skills/ios/hig-technologies/references/photo-editing.md +40 -0
- package/skills/ios/hig-technologies/references/researchkit.md +134 -0
- package/skills/ios/hig-technologies/references/shareplay.md +142 -0
- package/skills/ios/hig-technologies/references/shazamkit.md +47 -0
- package/skills/ios/hig-technologies/references/sign-in-with-apple.md +288 -0
- package/skills/ios/hig-technologies/references/siri.md +523 -0
- package/skills/ios/hig-technologies/references/tap-to-pay-on-iphone.md +208 -0
- package/skills/ios/hig-technologies/references/voiceover.md +90 -0
- package/skills/ios/hig-technologies/references/wallet.md +420 -0
- package/skills/ios/ios-26-platform/SKILL.md +53 -0
- package/skills/ios/ios-26-platform/references/automatic-adoption.md +161 -0
- package/skills/ios/ios-26-platform/references/backward-compat.md +238 -0
- package/skills/ios/ios-26-platform/references/liquid-glass.md +255 -0
- package/skills/ios/ios-26-platform/references/swiftui-apis.md +277 -0
- package/skills/ios/ios-26-platform/references/toolbar-navigation.md +250 -0
- package/skills/ios/ios-bootstrap/SKILL.md +107 -0
- package/skills/ios/ios-bootstrap/references/apple-docs-mcp-config.md +28 -0
- package/skills/ios/ios-bootstrap/references/new-project-dialog.md +41 -0
- package/skills/ios/ios-bootstrap/references/xcode-mcp-config.md +29 -0
- package/skills/ios/ios-debugger-agent/LICENSE +21 -0
- package/skills/ios/ios-debugger-agent/SKILL.md +58 -0
- package/skills/ios/ios-debugger-agent/agents/openai.yaml +4 -0
- package/skills/ios/ios-entitlements-generator/SKILL.md +47 -0
- package/skills/ios/ios-info-plist-hardening/SKILL.md +130 -0
- package/skills/ios/ios-maestro-flow-author/SKILL.md +68 -0
- package/skills/ios/ios-maestro-flow-author/references/input-and-scroll.yaml +17 -0
- package/skills/ios/ios-maestro-flow-author/references/modal-and-dismiss.yaml +14 -0
- package/skills/ios/ios-maestro-flow-author/references/onboarding-flow.yaml +16 -0
- package/skills/ios/ios-maestro-flow-author/references/tab-navigation.yaml +13 -0
- package/skills/ios/ios-maestro-flow-author/references/tap-and-assert.yaml +9 -0
- package/skills/ios/swift-accessibility/LICENSE +21 -0
- package/skills/ios/swift-accessibility/SKILL.md +371 -0
- package/skills/ios/swift-accessibility/examples/before-after-appkit.md +446 -0
- package/skills/ios/swift-accessibility/examples/before-after-swiftui.md +441 -0
- package/skills/ios/swift-accessibility/examples/before-after-uikit.md +464 -0
- package/skills/ios/swift-accessibility/references/assistive-access.md +441 -0
- package/skills/ios/swift-accessibility/references/display-settings.md +491 -0
- package/skills/ios/swift-accessibility/references/dynamic-type.md +420 -0
- package/skills/ios/swift-accessibility/references/media-accessibility.md +421 -0
- package/skills/ios/swift-accessibility/references/motor-input.md +393 -0
- package/skills/ios/swift-accessibility/references/nutrition-labels.md +362 -0
- package/skills/ios/swift-accessibility/references/platform-specifics.md +515 -0
- package/skills/ios/swift-accessibility/references/semantic-structure.md +585 -0
- package/skills/ios/swift-accessibility/references/testing-auditing.md +507 -0
- package/skills/ios/swift-accessibility/references/voice-control.md +317 -0
- package/skills/ios/swift-accessibility/references/voiceover-swiftui.md +584 -0
- package/skills/ios/swift-accessibility/references/voiceover-uikit.md +519 -0
- package/skills/ios/swift-accessibility/references/wcag-mapping.md +167 -0
- package/skills/ios/swift-accessibility/resources/audit-template.swift +128 -0
- package/skills/ios/swift-accessibility/resources/qa-checklist.md +258 -0
- package/skills/ios/swift-actor-persistence/SKILL.md +143 -0
- package/skills/ios/swift-concurrency/LICENSE +21 -0
- package/skills/ios/swift-concurrency/SKILL.md +171 -0
- package/skills/ios/swift-concurrency/references/_index.md +50 -0
- package/skills/ios/swift-concurrency/references/actors.md +660 -0
- package/skills/ios/swift-concurrency/references/async-algorithms.md +847 -0
- package/skills/ios/swift-concurrency/references/async-await-basics.md +266 -0
- package/skills/ios/swift-concurrency/references/async-sequences.md +710 -0
- package/skills/ios/swift-concurrency/references/core-data.md +560 -0
- package/skills/ios/swift-concurrency/references/glossary.md +135 -0
- package/skills/ios/swift-concurrency/references/linting.md +155 -0
- package/skills/ios/swift-concurrency/references/memory-management.md +569 -0
- package/skills/ios/swift-concurrency/references/migration.md +1104 -0
- package/skills/ios/swift-concurrency/references/performance.md +593 -0
- package/skills/ios/swift-concurrency/references/sendable.md +598 -0
- package/skills/ios/swift-concurrency/references/tasks.md +636 -0
- package/skills/ios/swift-concurrency/references/testing.md +592 -0
- package/skills/ios/swift-concurrency/references/threading.md +495 -0
- package/skills/ios/swift-concurrency-6-2/SKILL.md +216 -0
- package/skills/ios/swift-protocol-di-testing/SKILL.md +190 -0
- package/skills/ios/swift-security-expert/LICENSE +21 -0
- package/skills/ios/swift-security-expert/SKILL.md +470 -0
- package/skills/ios/swift-security-expert/references/biometric-authentication.md +565 -0
- package/skills/ios/swift-security-expert/references/certificate-trust.md +592 -0
- package/skills/ios/swift-security-expert/references/common-anti-patterns.md +690 -0
- package/skills/ios/swift-security-expert/references/compliance-owasp-mapping.md +537 -0
- package/skills/ios/swift-security-expert/references/credential-storage-patterns.md +721 -0
- package/skills/ios/swift-security-expert/references/cryptokit-public-key.md +505 -0
- package/skills/ios/swift-security-expert/references/cryptokit-symmetric.md +497 -0
- package/skills/ios/swift-security-expert/references/keychain-access-control.md +508 -0
- package/skills/ios/swift-security-expert/references/keychain-fundamentals.md +596 -0
- package/skills/ios/swift-security-expert/references/keychain-item-classes.md +476 -0
- package/skills/ios/swift-security-expert/references/keychain-sharing.md +458 -0
- package/skills/ios/swift-security-expert/references/migration-legacy-stores.md +727 -0
- package/skills/ios/swift-security-expert/references/secure-enclave.md +539 -0
- package/skills/ios/swift-security-expert/references/testing-security-code.md +781 -0
- package/skills/ios/swift-testing-expert/LICENSE +21 -0
- package/skills/ios/swift-testing-expert/SKILL.md +79 -0
- package/skills/ios/swift-testing-expert/references/_index.md +12 -0
- package/skills/ios/swift-testing-expert/references/async-testing-and-waiting.md +127 -0
- package/skills/ios/swift-testing-expert/references/expectations.md +145 -0
- package/skills/ios/swift-testing-expert/references/fundamentals.md +141 -0
- package/skills/ios/swift-testing-expert/references/migration-from-xctest.md +127 -0
- package/skills/ios/swift-testing-expert/references/parallelization-and-isolation.md +95 -0
- package/skills/ios/swift-testing-expert/references/parameterized-testing.md +284 -0
- package/skills/ios/swift-testing-expert/references/performance-and-best-practices.md +187 -0
- package/skills/ios/swift-testing-expert/references/traits-and-tags.md +114 -0
- package/skills/ios/swift-testing-expert/references/xcode-workflows.md +70 -0
- package/skills/ios/swiftdata-pro/LICENSE +21 -0
- package/skills/ios/swiftdata-pro/SKILL.md +102 -0
- package/skills/ios/swiftdata-pro/agents/openai.yaml +10 -0
- package/skills/ios/swiftdata-pro/assets/swiftdata-pro-icon.png +0 -0
- package/skills/ios/swiftdata-pro/assets/swiftdata-pro-icon.svg +29 -0
- package/skills/ios/swiftdata-pro/references/class-inheritance.md +104 -0
- package/skills/ios/swiftdata-pro/references/cloudkit.md +10 -0
- package/skills/ios/swiftdata-pro/references/core-rules.md +20 -0
- package/skills/ios/swiftdata-pro/references/indexing.md +27 -0
- package/skills/ios/swiftdata-pro/references/predicates.md +73 -0
- package/skills/ios/swiftui-design-principles/AGENTS.md +21 -0
- package/skills/ios/swiftui-design-principles/LICENSE +21 -0
- package/skills/ios/swiftui-design-principles/README.md +41 -0
- package/skills/ios/swiftui-design-principles/SKILL.md +605 -0
- package/skills/ios/swiftui-design-principles/metadata.json +10 -0
- package/skills/ios/swiftui-design-tokens/SKILL.md +475 -0
- package/skills/ios/swiftui-liquid-glass/LICENSE +21 -0
- package/skills/ios/swiftui-liquid-glass/SKILL.md +95 -0
- package/skills/ios/swiftui-liquid-glass/agents/openai.yaml +4 -0
- package/skills/ios/swiftui-liquid-glass/references/liquid-glass.md +280 -0
- package/skills/ios/swiftui-performance-audit/LICENSE +21 -0
- package/skills/ios/swiftui-performance-audit/SKILL.md +111 -0
- package/skills/ios/swiftui-performance-audit/agents/openai.yaml +4 -0
- package/skills/ios/swiftui-performance-audit/references/code-smells.md +150 -0
- package/skills/ios/swiftui-performance-audit/references/demystify-swiftui-performance-wwdc23.md +46 -0
- package/skills/ios/swiftui-performance-audit/references/optimizing-swiftui-performance-instruments.md +29 -0
- package/skills/ios/swiftui-performance-audit/references/profiling-intake.md +44 -0
- package/skills/ios/swiftui-performance-audit/references/report-template.md +47 -0
- package/skills/ios/swiftui-performance-audit/references/understanding-hangs-in-your-app.md +33 -0
- package/skills/ios/swiftui-performance-audit/references/understanding-improving-swiftui-performance.md +52 -0
- package/skills/ios/swiftui-pro/LICENSE +21 -0
- package/skills/ios/swiftui-pro/SKILL.md +108 -0
- package/skills/ios/swiftui-pro/agents/openai.yaml +10 -0
- package/skills/ios/swiftui-pro/assets/swiftui-pro-icon.png +0 -0
- package/skills/ios/swiftui-pro/assets/swiftui-pro-icon.svg +29 -0
- package/skills/ios/swiftui-pro/references/accessibility.md +13 -0
- package/skills/ios/swiftui-pro/references/api.md +39 -0
- package/skills/ios/swiftui-pro/references/data.md +43 -0
- package/skills/ios/swiftui-pro/references/design.md +31 -0
- package/skills/ios/swiftui-pro/references/hygiene.md +9 -0
- package/skills/ios/swiftui-pro/references/navigation.md +14 -0
- package/skills/ios/swiftui-pro/references/performance.md +46 -0
- package/skills/ios/swiftui-pro/references/swift.md +56 -0
- package/skills/ios/swiftui-pro/references/views.md +35 -0
- package/skills/ios/swiftui-ui-patterns/LICENSE +21 -0
- package/skills/ios/swiftui-ui-patterns/SKILL.md +100 -0
- package/skills/ios/swiftui-ui-patterns/agents/openai.yaml +4 -0
- package/skills/ios/swiftui-ui-patterns/references/app-wiring.md +201 -0
- package/skills/ios/swiftui-ui-patterns/references/async-state.md +96 -0
- package/skills/ios/swiftui-ui-patterns/references/components-index.md +50 -0
- package/skills/ios/swiftui-ui-patterns/references/controls.md +57 -0
- package/skills/ios/swiftui-ui-patterns/references/deeplinks.md +66 -0
- package/skills/ios/swiftui-ui-patterns/references/focus.md +90 -0
- package/skills/ios/swiftui-ui-patterns/references/form.md +97 -0
- package/skills/ios/swiftui-ui-patterns/references/grids.md +71 -0
- package/skills/ios/swiftui-ui-patterns/references/haptics.md +71 -0
- package/skills/ios/swiftui-ui-patterns/references/input-toolbar.md +51 -0
- package/skills/ios/swiftui-ui-patterns/references/lightweight-clients.md +93 -0
- package/skills/ios/swiftui-ui-patterns/references/list.md +86 -0
- package/skills/ios/swiftui-ui-patterns/references/loading-placeholders.md +38 -0
- package/skills/ios/swiftui-ui-patterns/references/macos-settings.md +71 -0
- package/skills/ios/swiftui-ui-patterns/references/matched-transitions.md +59 -0
- package/skills/ios/swiftui-ui-patterns/references/media.md +73 -0
- package/skills/ios/swiftui-ui-patterns/references/menu-bar.md +101 -0
- package/skills/ios/swiftui-ui-patterns/references/navigationstack.md +159 -0
- package/skills/ios/swiftui-ui-patterns/references/overlay.md +45 -0
- package/skills/ios/swiftui-ui-patterns/references/performance.md +62 -0
- package/skills/ios/swiftui-ui-patterns/references/previews.md +48 -0
- package/skills/ios/swiftui-ui-patterns/references/scroll-reveal.md +133 -0
- package/skills/ios/swiftui-ui-patterns/references/scrollview.md +87 -0
- package/skills/ios/swiftui-ui-patterns/references/searchable.md +71 -0
- package/skills/ios/swiftui-ui-patterns/references/sheets.md +155 -0
- package/skills/ios/swiftui-ui-patterns/references/split-views.md +72 -0
- package/skills/ios/swiftui-ui-patterns/references/tabview.md +114 -0
- package/skills/ios/swiftui-ui-patterns/references/theming.md +71 -0
- package/skills/ios/swiftui-ui-patterns/references/title-menus.md +93 -0
- package/skills/ios/swiftui-ui-patterns/references/top-bar.md +49 -0
- package/skills/ios/swiftui-view-refactor/LICENSE +21 -0
- package/skills/ios/swiftui-view-refactor/SKILL.md +207 -0
- package/skills/ios/swiftui-view-refactor/agents/openai.yaml +4 -0
- package/skills/ios/swiftui-view-refactor/references/mv-patterns.md +161 -0
- package/skills/ios/widgetkit/LICENSE +131 -0
- package/skills/ios/widgetkit/SKILL.md +502 -0
- package/skills/ios/widgetkit/references/widgetkit-advanced.md +871 -0
- package/skills/ios/writing-for-interfaces/SKILL.md +75 -0
- package/skills/web/accessibility/SKILL.md +146 -0
- package/skills/web/aceternity-ui/SKILL.md +719 -0
- package/skills/web/aceternity-ui/metadata.json +10 -0
- package/skills/web/api-design/SKILL.md +523 -0
- package/skills/web/chart-accessibility/SKILL.md +332 -0
- package/skills/web/composition-patterns/AGENTS.md +946 -0
- package/skills/web/composition-patterns/README.md +60 -0
- package/skills/web/composition-patterns/SKILL.md +89 -0
- package/skills/web/composition-patterns/metadata.json +11 -0
- package/skills/web/composition-patterns/rules/_sections.md +29 -0
- package/skills/web/composition-patterns/rules/_template.md +24 -0
- package/skills/web/composition-patterns/rules/architecture-avoid-boolean-props.md +100 -0
- package/skills/web/composition-patterns/rules/architecture-compound-components.md +112 -0
- package/skills/web/composition-patterns/rules/patterns-children-over-render-props.md +87 -0
- package/skills/web/composition-patterns/rules/patterns-explicit-variants.md +100 -0
- package/skills/web/composition-patterns/rules/react19-no-forwardref.md +42 -0
- package/skills/web/composition-patterns/rules/state-context-interface.md +191 -0
- package/skills/web/composition-patterns/rules/state-decouple-implementation.md +113 -0
- package/skills/web/composition-patterns/rules/state-lift-state.md +125 -0
- package/skills/web/cost-aware-llm-pipeline/SKILL.md +183 -0
- package/skills/web/database-migrations/SKILL.md +429 -0
- package/skills/web/deployment-patterns/SKILL.md +427 -0
- package/skills/web/docker-patterns/SKILL.md +364 -0
- package/skills/web/e2e-testing/SKILL.md +326 -0
- package/skills/web/lighthouse-ci/SKILL.md +361 -0
- package/skills/web/mcp-server-patterns/SKILL.md +69 -0
- package/skills/web/next-best-practices/SKILL.md +153 -0
- package/skills/web/next-best-practices/async-patterns.md +87 -0
- package/skills/web/next-best-practices/bundling.md +180 -0
- package/skills/web/next-best-practices/data-patterns.md +297 -0
- package/skills/web/next-best-practices/debug-tricks.md +105 -0
- package/skills/web/next-best-practices/directives.md +73 -0
- package/skills/web/next-best-practices/error-handling.md +227 -0
- package/skills/web/next-best-practices/file-conventions.md +140 -0
- package/skills/web/next-best-practices/font.md +245 -0
- package/skills/web/next-best-practices/functions.md +108 -0
- package/skills/web/next-best-practices/hydration-error.md +91 -0
- package/skills/web/next-best-practices/image.md +173 -0
- package/skills/web/next-best-practices/metadata.md +301 -0
- package/skills/web/next-best-practices/parallel-routes.md +287 -0
- package/skills/web/next-best-practices/route-handlers.md +146 -0
- package/skills/web/next-best-practices/rsc-boundaries.md +159 -0
- package/skills/web/next-best-practices/runtime-selection.md +39 -0
- package/skills/web/next-best-practices/scripts.md +141 -0
- package/skills/web/next-best-practices/self-hosting.md +371 -0
- package/skills/web/next-best-practices/suspense-boundaries.md +67 -0
- package/skills/web/next-cache-components/SKILL.md +411 -0
- package/skills/web/postgres-best-practices/SKILL.md +14 -0
- package/skills/web/postgres-best-practices/references/schema-design.md +9 -0
- package/skills/web/react-best-practices/AGENTS.md +3810 -0
- package/skills/web/react-best-practices/README.md +123 -0
- package/skills/web/react-best-practices/SKILL.md +149 -0
- package/skills/web/react-best-practices/metadata.json +15 -0
- package/skills/web/react-best-practices/rules/_sections.md +46 -0
- package/skills/web/react-best-practices/rules/_template.md +28 -0
- package/skills/web/react-best-practices/rules/advanced-effect-event-deps.md +56 -0
- package/skills/web/react-best-practices/rules/advanced-event-handler-refs.md +55 -0
- package/skills/web/react-best-practices/rules/advanced-init-once.md +42 -0
- package/skills/web/react-best-practices/rules/advanced-use-latest.md +39 -0
- package/skills/web/react-best-practices/rules/async-api-routes.md +38 -0
- package/skills/web/react-best-practices/rules/async-cheap-condition-before-await.md +37 -0
- package/skills/web/react-best-practices/rules/async-defer-await.md +82 -0
- package/skills/web/react-best-practices/rules/async-dependencies.md +51 -0
- package/skills/web/react-best-practices/rules/async-parallel.md +28 -0
- package/skills/web/react-best-practices/rules/async-suspense-boundaries.md +99 -0
- package/skills/web/react-best-practices/rules/bundle-analyzable-paths.md +63 -0
- package/skills/web/react-best-practices/rules/bundle-barrel-imports.md +60 -0
- package/skills/web/react-best-practices/rules/bundle-conditional.md +31 -0
- package/skills/web/react-best-practices/rules/bundle-defer-third-party.md +49 -0
- package/skills/web/react-best-practices/rules/bundle-dynamic-imports.md +35 -0
- package/skills/web/react-best-practices/rules/bundle-preload.md +50 -0
- package/skills/web/react-best-practices/rules/client-event-listeners.md +74 -0
- package/skills/web/react-best-practices/rules/client-localstorage-schema.md +71 -0
- package/skills/web/react-best-practices/rules/client-passive-event-listeners.md +48 -0
- package/skills/web/react-best-practices/rules/client-swr-dedup.md +56 -0
- package/skills/web/react-best-practices/rules/js-batch-dom-css.md +107 -0
- package/skills/web/react-best-practices/rules/js-cache-function-results.md +80 -0
- package/skills/web/react-best-practices/rules/js-cache-property-access.md +28 -0
- package/skills/web/react-best-practices/rules/js-cache-storage.md +70 -0
- package/skills/web/react-best-practices/rules/js-combine-iterations.md +32 -0
- package/skills/web/react-best-practices/rules/js-early-exit.md +50 -0
- package/skills/web/react-best-practices/rules/js-flatmap-filter.md +60 -0
- package/skills/web/react-best-practices/rules/js-hoist-regexp.md +45 -0
- package/skills/web/react-best-practices/rules/js-index-maps.md +37 -0
- package/skills/web/react-best-practices/rules/js-length-check-first.md +49 -0
- package/skills/web/react-best-practices/rules/js-min-max-loop.md +82 -0
- package/skills/web/react-best-practices/rules/js-request-idle-callback.md +105 -0
- package/skills/web/react-best-practices/rules/js-set-map-lookups.md +24 -0
- package/skills/web/react-best-practices/rules/js-tosorted-immutable.md +57 -0
- package/skills/web/react-best-practices/rules/rendering-activity.md +26 -0
- package/skills/web/react-best-practices/rules/rendering-animate-svg-wrapper.md +47 -0
- package/skills/web/react-best-practices/rules/rendering-conditional-render.md +40 -0
- package/skills/web/react-best-practices/rules/rendering-content-visibility.md +38 -0
- package/skills/web/react-best-practices/rules/rendering-hoist-jsx.md +46 -0
- package/skills/web/react-best-practices/rules/rendering-hydration-no-flicker.md +82 -0
- package/skills/web/react-best-practices/rules/rendering-hydration-suppress-warning.md +30 -0
- package/skills/web/react-best-practices/rules/rendering-resource-hints.md +85 -0
- package/skills/web/react-best-practices/rules/rendering-script-defer-async.md +68 -0
- package/skills/web/react-best-practices/rules/rendering-svg-precision.md +28 -0
- package/skills/web/react-best-practices/rules/rendering-usetransition-loading.md +75 -0
- package/skills/web/react-best-practices/rules/rerender-defer-reads.md +39 -0
- package/skills/web/react-best-practices/rules/rerender-dependencies.md +45 -0
- package/skills/web/react-best-practices/rules/rerender-derived-state-no-effect.md +40 -0
- package/skills/web/react-best-practices/rules/rerender-derived-state.md +29 -0
- package/skills/web/react-best-practices/rules/rerender-functional-setstate.md +74 -0
- package/skills/web/react-best-practices/rules/rerender-lazy-state-init.md +58 -0
- package/skills/web/react-best-practices/rules/rerender-memo-with-default-value.md +38 -0
- package/skills/web/react-best-practices/rules/rerender-memo.md +44 -0
- package/skills/web/react-best-practices/rules/rerender-move-effect-to-event.md +45 -0
- package/skills/web/react-best-practices/rules/rerender-no-inline-components.md +82 -0
- package/skills/web/react-best-practices/rules/rerender-simple-expression-in-memo.md +35 -0
- package/skills/web/react-best-practices/rules/rerender-split-combined-hooks.md +64 -0
- package/skills/web/react-best-practices/rules/rerender-transitions.md +40 -0
- package/skills/web/react-best-practices/rules/rerender-use-deferred-value.md +59 -0
- package/skills/web/react-best-practices/rules/rerender-use-ref-transient-values.md +73 -0
- package/skills/web/react-best-practices/rules/server-after-nonblocking.md +73 -0
- package/skills/web/react-best-practices/rules/server-auth-actions.md +96 -0
- package/skills/web/react-best-practices/rules/server-cache-lru.md +41 -0
- package/skills/web/react-best-practices/rules/server-cache-react.md +76 -0
- package/skills/web/react-best-practices/rules/server-dedup-props.md +65 -0
- package/skills/web/react-best-practices/rules/server-hoist-static-io.md +149 -0
- package/skills/web/react-best-practices/rules/server-no-shared-module-state.md +50 -0
- package/skills/web/react-best-practices/rules/server-parallel-fetching.md +83 -0
- package/skills/web/react-best-practices/rules/server-parallel-nested-fetching.md +34 -0
- package/skills/web/react-best-practices/rules/server-serialization.md +38 -0
- package/skills/web/seo/SKILL.md +154 -0
- package/skills/web/web-design-guidelines/SKILL.md +39 -0
- package/skills/web/zap-scan-config/SKILL.md +444 -0
- package/skills/web/zap-scan-config/assets/.gitkeep +9 -0
- package/skills/web/zap-scan-config/assets/github_action.yml +207 -0
- package/skills/web/zap-scan-config/assets/gitlab_ci.yml +226 -0
- package/skills/web/zap-scan-config/assets/zap_automation.yaml +196 -0
- package/skills/web/zap-scan-config/assets/zap_context.xml +192 -0
- package/skills/web/zap-scan-config/references/EXAMPLE.md +40 -0
- package/skills/web/zap-scan-config/references/api_testing_guide.md +475 -0
- package/skills/web/zap-scan-config/references/authentication_guide.md +431 -0
- package/skills/web/zap-scan-config/references/false_positive_handling.md +427 -0
- package/skills/web/zap-scan-config/references/owasp_mapping.md +255 -0
- package/src/lrr/aggregator.ts +80 -0
- package/src/orchestrator/hooks/context-header.ts +95 -0
- package/src/orchestrator/hooks/token-accounting-emitter.ts +77 -0
- package/src/orchestrator/hooks/token-accounting.ts +101 -0
- package/src/orchestrator/mcp/cycle-counter.ts +129 -0
- package/src/orchestrator/mcp/scribe.ts +283 -0
- package/src/orchestrator/mcp/state-save.ts +149 -0
- package/src/orchestrator/mcp/write-lease.ts +167 -0
- package/src/orchestrator/phase4-shared-context.ts +41 -0
- package/src/orchestrator/schemas/backward-edge.ts +46 -0
- package/agents/agentic-identity-trust.md +0 -121
- package/agents/data-consolidation-agent.md +0 -39
- package/agents/design-image-prompt-engineer.md +0 -105
- package/agents/design-visual-storyteller.md +0 -147
- package/agents/design-whimsy-injector.md +0 -89
- package/agents/engineering-autonomous-optimization-architect.md +0 -105
- package/agents/market-intel.md +0 -35
- package/agents/marketing-instagram-curator.md +0 -111
- package/agents/marketing-reddit-community-builder.md +0 -121
- package/agents/marketing-social-media-strategist.md +0 -74
- package/agents/marketing-tiktok-strategist.md +0 -123
- package/agents/marketing-twitter-engager.md +0 -124
- package/agents/marketing-wechat-official-account.md +0 -143
- package/agents/marketing-xiaohongshu-specialist.md +0 -136
- package/agents/marketing-zhihu-strategist.md +0 -160
- package/agents/product-behavioral-nudge-engine.md +0 -78
- package/agents/project-management-experiment-tracker.md +0 -102
- package/agents/report-distribution-agent.md +0 -43
- package/agents/risk-analysis.md +0 -45
- package/agents/sales-data-extraction-agent.md +0 -46
- package/agents/specialized-cultural-intelligence-strategist.md +0 -65
- package/agents/specialized-developer-advocate.md +0 -146
- package/agents/support-analytics-reporter.md +0 -133
- package/agents/support-executive-summary-generator.md +0 -64
- package/agents/support-finance-tracker.md +0 -145
- package/agents/support-legal-compliance-checker.md +0 -129
- package/agents/support-support-responder.md +0 -91
- package/agents/testing-accessibility-auditor.md +0 -110
- package/agents/testing-test-results-analyzer.md +0 -97
- package/agents/testing-tool-evaluator.md +0 -76
- package/agents/testing-workflow-optimizer.md +0 -99
- package/agents/user-research.md +0 -40
|
@@ -0,0 +1,727 @@
|
|
|
1
|
+
# Migration & Legacy Stores
|
|
2
|
+
|
|
3
|
+
> **Scope:** Migrating sensitive data from UserDefaults, plists, NSCoding archives, and other insecure storage to Apple Keychain Services. Covers secure deletion of legacy data, first-launch keychain cleanup, versioned migration patterns, and the Team ID transfer edge case.
|
|
4
|
+
>
|
|
5
|
+
> **Applies to:** iOS 15+ (actor support, pre-warming), iOS 17+ (recommended deployment target)
|
|
6
|
+
>
|
|
7
|
+
> **Cross-references:** `keychain-fundamentals.md` (SecItem CRUD), `keychain-access-control.md` (accessibility classes), `common-anti-patterns.md` (UserDefaults secrets anti-pattern), `credential-storage-patterns.md` (token lifecycle post-migration), `testing-security-code.md` (protocol-based mocking)
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Why Migrate — The Risk of Legacy Storage
|
|
12
|
+
|
|
13
|
+
UserDefaults, `.plist` files, and NSCoding archives store data as unencrypted plaintext within the app sandbox. This data is readable on jailbroken devices and included in unencrypted iTunes/Finder backups — anyone with backup access can extract tokens, passwords, and PII. OWASP ranks insecure data storage as a top-10 mobile risk (M9).
|
|
14
|
+
|
|
15
|
+
| Store | Encrypted at rest | In backups | Survives app uninstall | Suitable for secrets |
|
|
16
|
+
| ----------------- | ----------------- | ---------------------------------- | ---------------------- | -------------------- |
|
|
17
|
+
| UserDefaults | No | Yes | No | **No** |
|
|
18
|
+
| .plist files | No (default) | Yes | No | **No** |
|
|
19
|
+
| NSCoding archives | No (default) | Yes | No | **No** |
|
|
20
|
+
| Keychain | Yes (AES-256-GCM) | `ThisDeviceOnly` variants excluded | **Yes** | **Yes** |
|
|
21
|
+
|
|
22
|
+
Keychain items are managed by the `securityd` daemon, encrypted with per-row keys protected by the Secure Enclave, and isolated from the app sandbox. This is the only appropriate location for tokens, passwords, API keys, and PII on Apple platforms.
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## The Five Correctness Traps
|
|
27
|
+
|
|
28
|
+
Most AI-generated migration code contains at least one of these errors. Each passes testing but fails catastrophically in production.
|
|
29
|
+
|
|
30
|
+
**Trap 1 — Legacy data survives after migration.** Calling `UserDefaults.standard.removeObject(forKey:)` removes the key-value pair from the in-memory cache and plist file, but does not securely overwrite NAND flash. However, iOS achieves secure deletion through _cryptographic erasure_: every file has a per-file AES-256 key, and standard deletion APIs destroy that key via Effaceable Storage, rendering physical bits permanently inaccessible. The real risk vector is **unencrypted backups** created before migration completes — the plist stays on disk until the filesystem reclaims space. **Always delete all legacy keys explicitly after verified keychain writes.**
|
|
31
|
+
|
|
32
|
+
**Trap 2 — Keychain items survive app deletion.** When a user uninstalls your app, UserDefaults and sandbox files are wiped, but keychain items persist indefinitely. Apple attempted to change this in iOS 10.3 betas but reverted due to compatibility issues. On reinstall, stale keychain items (old tokens, expired credentials, outdated schemas) cause silent authentication failures or — worse — restore a _previous user's_ session.
|
|
33
|
+
|
|
34
|
+
**Trap 3 — Migration runs on every launch.** Checking UserDefaults for legacy data on every launch wastes cycles and risks data loss during iOS 15+ app pre-warming. When the system pre-warms your process before the device is unlocked, `UserDefaults` may return empty values (the encrypted plist is inaccessible). A migration that interprets empty results as "nothing to migrate" will skip real data or overwrite valid keychain entries with nil.
|
|
35
|
+
|
|
36
|
+
**Trap 4 — Non-atomic migration leaves data in limbo.** Writing to keychain then deleting from UserDefaults as two independent operations creates a failure window. If the app is killed between write and delete — or the keychain write silently fails — users lose their data entirely.
|
|
37
|
+
|
|
38
|
+
**Trap 5 — Changing `kSecAttrService` or `kSecAttrAccount` orphans existing items.** These attributes form the primary key for `kSecClassGenericPassword`. Changing either in a new version doesn't update existing items — it creates new ones. The old items become invisible orphans that waste keychain space and cause `errSecDuplicateItem` in unexpected contexts. Critically, `SecItemUpdate` **cannot change primary key attributes** — the call will error. You must perform a full rekey migration: read old → write new → verify → delete old.
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## First-Launch Keychain Cleanup
|
|
43
|
+
|
|
44
|
+
The persistence asymmetry (UserDefaults deleted on uninstall, keychain not) enables a reliable reinstall detector. This pattern **must run before any other keychain or SDK initialization** — Firebase, analytics, and auth libraries all read keychain items during setup.
|
|
45
|
+
|
|
46
|
+
```swift
|
|
47
|
+
// ✅ CORRECT: First-launch cleanup with protected data guard
|
|
48
|
+
// iOS 15+ required for isProtectedDataAvailable / pre-warming behavior
|
|
49
|
+
|
|
50
|
+
actor FirstLaunchGuard {
|
|
51
|
+
static let shared = FirstLaunchGuard()
|
|
52
|
+
private let hasRunKey = "com.myapp.hasCompletedFirstLaunch"
|
|
53
|
+
|
|
54
|
+
/// Call at the very start of app lifecycle, before SDK initialization.
|
|
55
|
+
func performCleanupIfNeeded() async {
|
|
56
|
+
let isSubsequentRun = UserDefaults.standard.bool(forKey: hasRunKey)
|
|
57
|
+
guard !isSubsequentRun else { return }
|
|
58
|
+
|
|
59
|
+
// iOS 15+ pre-warming guard: device may still be locked
|
|
60
|
+
guard await isProtectedDataAvailable() else {
|
|
61
|
+
await waitForProtectedData()
|
|
62
|
+
return
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Wipe stale keychain items from a previous installation
|
|
66
|
+
deleteAllKeychainItems()
|
|
67
|
+
|
|
68
|
+
// Set flag so this only runs once per install
|
|
69
|
+
UserDefaults.standard.set(true, forKey: hasRunKey)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
private func deleteAllKeychainItems() {
|
|
73
|
+
let classes: [CFString] = [
|
|
74
|
+
kSecClassGenericPassword, kSecClassInternetPassword,
|
|
75
|
+
kSecClassCertificate, kSecClassKey, kSecClassIdentity
|
|
76
|
+
]
|
|
77
|
+
for itemClass in classes {
|
|
78
|
+
let query: NSDictionary = [
|
|
79
|
+
kSecClass: itemClass,
|
|
80
|
+
kSecAttrSynchronizable: kSecAttrSynchronizableAny
|
|
81
|
+
]
|
|
82
|
+
SecItemDelete(query)
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
private func isProtectedDataAvailable() async -> Bool {
|
|
87
|
+
await MainActor.run {
|
|
88
|
+
UIApplication.shared.isProtectedDataAvailable
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
private func waitForProtectedData() async {
|
|
93
|
+
await withCheckedContinuation { continuation in
|
|
94
|
+
NotificationCenter.default.addObserver(
|
|
95
|
+
forName: UIApplication.protectedDataDidBecomeAvailableNotification,
|
|
96
|
+
object: nil, queue: .main
|
|
97
|
+
) { _ in
|
|
98
|
+
Task {
|
|
99
|
+
self.deleteAllKeychainItems()
|
|
100
|
+
UserDefaults.standard.set(true, forKey: self.hasRunKey)
|
|
101
|
+
continuation.resume()
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
```swift
|
|
110
|
+
// ❌ INCORRECT: No first-launch cleanup — stale keychain from previous install
|
|
111
|
+
@main
|
|
112
|
+
struct BrokenApp: App {
|
|
113
|
+
init() {
|
|
114
|
+
// Reads keychain without checking for stale data
|
|
115
|
+
if let token = try? keychainRead(service: "com.myapp", account: "authToken") {
|
|
116
|
+
// This token might be from a PREVIOUS user who deleted the app.
|
|
117
|
+
// The new user inherits someone else's session.
|
|
118
|
+
AuthManager.shared.restoreSession(token: token)
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
var body: some Scene { WindowGroup { ContentView() } }
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
The `isProtectedDataAvailable` check is critical. iOS 15 introduced app pre-warming — the system can launch your process before the user unlocks the device. During pre-warming, both UserDefaults and keychain items with `kSecAttrAccessibleWhenUnlocked` are unavailable. Multiple high-profile apps (including Twitter) suffered mass user logouts on iOS 15 because their startup code interpreted empty data during pre-warm as "no credentials" and wiped sessions.
|
|
126
|
+
|
|
127
|
+
> **Include `kSecAttrSynchronizableAny`** in cleanup queries. Without it, `SecItemDelete` skips iCloud-synced items, leaving them as invisible ghosts.
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
## Atomic Migration: Read → Write → Verify → Delete
|
|
132
|
+
|
|
133
|
+
The most dangerous pattern is deleting legacy data before confirming the keychain write succeeded. The correct sequence is always: **read → write → verify → delete**.
|
|
134
|
+
|
|
135
|
+
```swift
|
|
136
|
+
// ✅ CORRECT: Atomic per-key migration with verification and rollback
|
|
137
|
+
actor AtomicMigrator {
|
|
138
|
+
struct MigrationResult {
|
|
139
|
+
let key: String
|
|
140
|
+
let succeeded: Bool
|
|
141
|
+
let error: Error?
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
private let keychain: any MigrationKeychainProtocol
|
|
145
|
+
|
|
146
|
+
init(keychain: any MigrationKeychainProtocol) {
|
|
147
|
+
self.keychain = keychain
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/// Failed keys remain in UserDefaults for retry on next launch.
|
|
151
|
+
func migrateUserDefaultsKeys(
|
|
152
|
+
_ keys: [String],
|
|
153
|
+
service: String,
|
|
154
|
+
accessible: CFString = kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
|
|
155
|
+
) async -> [MigrationResult] {
|
|
156
|
+
var results: [MigrationResult] = []
|
|
157
|
+
|
|
158
|
+
for key in keys {
|
|
159
|
+
do {
|
|
160
|
+
// STEP 1: Read from legacy storage
|
|
161
|
+
guard let legacyValue = UserDefaults.standard.string(forKey: key),
|
|
162
|
+
let data = legacyValue.data(using: .utf8) else {
|
|
163
|
+
results.append(.init(key: key, succeeded: true, error: nil))
|
|
164
|
+
continue
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// STEP 2: Write to keychain (add-or-update handles duplicates)
|
|
168
|
+
try await keychain.save(data, service: service,
|
|
169
|
+
account: key, accessible: accessible)
|
|
170
|
+
|
|
171
|
+
// STEP 3: Verify by reading back
|
|
172
|
+
let readBack = try await keychain.read(service: service, account: key)
|
|
173
|
+
guard readBack == data else {
|
|
174
|
+
throw MigrationError.verificationFailed(key: key)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// STEP 4: Delete from UserDefaults ONLY after verified write
|
|
178
|
+
UserDefaults.standard.removeObject(forKey: key)
|
|
179
|
+
results.append(.init(key: key, succeeded: true, error: nil))
|
|
180
|
+
|
|
181
|
+
} catch {
|
|
182
|
+
// ROLLBACK: Leave UserDefaults intact for this key
|
|
183
|
+
results.append(.init(key: key, succeeded: false, error: error))
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return results
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
enum MigrationError: Error {
|
|
190
|
+
case verificationFailed(key: String)
|
|
191
|
+
case corruptArchive(path: String)
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
```swift
|
|
197
|
+
// ❌ INCORRECT: Deletes legacy data BEFORE verifying keychain write
|
|
198
|
+
func dangerousMigration() {
|
|
199
|
+
let keys = ["authToken", "refreshToken"]
|
|
200
|
+
for key in keys {
|
|
201
|
+
guard let value = UserDefaults.standard.string(forKey: key) else { continue }
|
|
202
|
+
|
|
203
|
+
// Deletes FIRST — if keychain write fails, data is gone forever
|
|
204
|
+
UserDefaults.standard.removeObject(forKey: key) // ← CATASTROPHIC
|
|
205
|
+
|
|
206
|
+
let query: [String: Any] = [
|
|
207
|
+
kSecClass as String: kSecClassGenericPassword,
|
|
208
|
+
kSecAttrService as String: "com.myapp",
|
|
209
|
+
kSecAttrAccount as String: key,
|
|
210
|
+
kSecValueData as String: value.data(using: .utf8)!
|
|
211
|
+
]
|
|
212
|
+
let status = SecItemAdd(query as CFDictionary, nil)
|
|
213
|
+
// If status != errSecSuccess, the token is permanently lost.
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
The migration is **idempotent by design**: already-migrated keys return `nil` from UserDefaults in Step 1 and are skipped. Failed keys retain their original values, ready for retry. This makes it safe to re-run after crash, app kill, or OOM termination.
|
|
219
|
+
|
|
220
|
+
---
|
|
221
|
+
|
|
222
|
+
## Versioned Migration with Schema Tracking
|
|
223
|
+
|
|
224
|
+
A production system needs version tracking to avoid re-running completed migrations and to handle users who skip versions. The schema version belongs in the **keychain** (survives reinstalls), not UserDefaults.
|
|
225
|
+
|
|
226
|
+
```swift
|
|
227
|
+
// ✅ CORRECT: Versioned chain migration with schema version in keychain
|
|
228
|
+
actor MigrationCoordinator {
|
|
229
|
+
static let shared = MigrationCoordinator()
|
|
230
|
+
|
|
231
|
+
private let serviceName = "com.myapp.credentials"
|
|
232
|
+
private let schemaVersionAccount = "com.myapp.schema.version"
|
|
233
|
+
private static let currentSchemaVersion: Int = 3
|
|
234
|
+
|
|
235
|
+
enum MigrationState {
|
|
236
|
+
case upToDate
|
|
237
|
+
case migrated(from: Int, to: Int)
|
|
238
|
+
case deferred(reason: String)
|
|
239
|
+
case failed(Error)
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
func migrateIfNeeded() async -> MigrationState {
|
|
243
|
+
// Guard: protected data must be available (pre-warming defense)
|
|
244
|
+
let dataAvailable = await MainActor.run {
|
|
245
|
+
UIApplication.shared.isProtectedDataAvailable
|
|
246
|
+
}
|
|
247
|
+
guard dataAvailable else {
|
|
248
|
+
return .deferred(reason: "Device locked — protected data unavailable")
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
let storedVersion = readSchemaVersion()
|
|
252
|
+
guard storedVersion < Self.currentSchemaVersion else { return .upToDate }
|
|
253
|
+
|
|
254
|
+
do {
|
|
255
|
+
// Chain migration: each step runs sequentially
|
|
256
|
+
if storedVersion < 1 {
|
|
257
|
+
try await migrateV0toV1_UserDefaultsToKeychain()
|
|
258
|
+
}
|
|
259
|
+
if storedVersion < 2 {
|
|
260
|
+
try await migrateV1toV2_NSCodingArchivesToKeychain()
|
|
261
|
+
}
|
|
262
|
+
if storedVersion < 3 {
|
|
263
|
+
try await migrateV2toV3_UpgradeAccessibilityClass()
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Update version ONLY after all steps succeed
|
|
267
|
+
try saveSchemaVersion(Self.currentSchemaVersion)
|
|
268
|
+
return .migrated(from: storedVersion, to: Self.currentSchemaVersion)
|
|
269
|
+
} catch {
|
|
270
|
+
// Do NOT update schema version — retry on next launch
|
|
271
|
+
os_log(.error, log: .migration,
|
|
272
|
+
"Migration failed: %{public}@", error.localizedDescription)
|
|
273
|
+
return .failed(error)
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// MARK: - Schema Version (stored in keychain, survives reinstall)
|
|
278
|
+
|
|
279
|
+
private func readSchemaVersion() -> Int {
|
|
280
|
+
guard let data = try? keychainRead(
|
|
281
|
+
service: serviceName, account: schemaVersionAccount),
|
|
282
|
+
let str = String(data: data, encoding: .utf8),
|
|
283
|
+
let version = Int(str) else { return 0 }
|
|
284
|
+
return version
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
private func saveSchemaVersion(_ version: Int) throws {
|
|
288
|
+
let data = "\(version)".data(using: .utf8)!
|
|
289
|
+
try keychainSave(data, service: serviceName,
|
|
290
|
+
account: schemaVersionAccount)
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// MARK: - V1: UserDefaults → Keychain
|
|
294
|
+
|
|
295
|
+
private func migrateV0toV1_UserDefaultsToKeychain() async throws {
|
|
296
|
+
let migrator = AtomicMigrator(keychain: KeychainManager.shared)
|
|
297
|
+
let results = await migrator.migrateUserDefaultsKeys(
|
|
298
|
+
["authToken", "refreshToken", "apiSecret"],
|
|
299
|
+
service: serviceName
|
|
300
|
+
)
|
|
301
|
+
// Check for critical failures (non-nil keys that didn't migrate)
|
|
302
|
+
let failures = results.filter { !$0.succeeded }
|
|
303
|
+
if !failures.isEmpty {
|
|
304
|
+
os_log(.error, log: .migration,
|
|
305
|
+
"V1 migration: %d keys failed", failures.count)
|
|
306
|
+
}
|
|
307
|
+
// Force-sync UserDefaults deletions to disk
|
|
308
|
+
UserDefaults.standard.synchronize()
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// MARK: - V2: NSCoding Archives → Keychain
|
|
312
|
+
|
|
313
|
+
private func migrateV1toV2_NSCodingArchivesToKeychain() async throws {
|
|
314
|
+
let documentsURL = FileManager.default.urls(
|
|
315
|
+
for: .documentDirectory, in: .userDomainMask).first!
|
|
316
|
+
let archiveURL = documentsURL.appendingPathComponent("UserSession.archive")
|
|
317
|
+
|
|
318
|
+
guard FileManager.default.fileExists(atPath: archiveURL.path) else { return }
|
|
319
|
+
|
|
320
|
+
let archiveData = try Data(contentsOf: archiveURL)
|
|
321
|
+
guard let session = try NSKeyedUnarchiver.unarchivedObject(
|
|
322
|
+
ofClass: LegacySession.self, from: archiveData) else {
|
|
323
|
+
throw AtomicMigrator.MigrationError.corruptArchive(path: archiveURL.path)
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
let sessionData = try JSONEncoder().encode(session.toModernSession())
|
|
327
|
+
try keychainSave(sessionData, service: serviceName, account: "userSession")
|
|
328
|
+
|
|
329
|
+
// Verify before deleting archive file
|
|
330
|
+
let verified = try keychainRead(service: serviceName, account: "userSession")
|
|
331
|
+
guard verified == sessionData else {
|
|
332
|
+
throw AtomicMigrator.MigrationError.verificationFailed(key: "userSession")
|
|
333
|
+
}
|
|
334
|
+
try FileManager.default.removeItem(at: archiveURL)
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// MARK: - V3: Upgrade accessibility class on existing items
|
|
338
|
+
|
|
339
|
+
private func migrateV2toV3_UpgradeAccessibilityClass() async throws {
|
|
340
|
+
let accounts = ["authToken", "refreshToken", "apiSecret", "userSession"]
|
|
341
|
+
for account in accounts {
|
|
342
|
+
guard let data = try? keychainRead(
|
|
343
|
+
service: serviceName, account: account) else { continue }
|
|
344
|
+
// Re-save with updated accessibility — add-or-update pattern
|
|
345
|
+
// updates the accessibility class via SecItemUpdate
|
|
346
|
+
try keychainSave(data, service: serviceName, account: account,
|
|
347
|
+
accessible: kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly)
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
private extension OSLog {
|
|
353
|
+
static let migration = OSLog(
|
|
354
|
+
subsystem: Bundle.main.bundleIdentifier ?? "com.myapp",
|
|
355
|
+
category: "KeychainMigration"
|
|
356
|
+
)
|
|
357
|
+
}
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
```swift
|
|
361
|
+
// ❌ INCORRECT: Runs every launch, no version check, no verification, no legacy delete
|
|
362
|
+
func brokenMigration() {
|
|
363
|
+
// No version check — runs every single launch
|
|
364
|
+
// No isProtectedDataAvailable check — fails during pre-warm
|
|
365
|
+
if let token = UserDefaults.standard.string(forKey: "authToken") {
|
|
366
|
+
let query: [String: Any] = [
|
|
367
|
+
kSecClass as String: kSecClassGenericPassword,
|
|
368
|
+
kSecAttrService as String: "com.myapp",
|
|
369
|
+
kSecAttrAccount as String: "authToken",
|
|
370
|
+
kSecValueData as String: token.data(using: .utf8)!
|
|
371
|
+
]
|
|
372
|
+
// No errSecDuplicateItem handling — crashes on second launch
|
|
373
|
+
SecItemAdd(query as CFDictionary, nil)
|
|
374
|
+
// Never deletes from UserDefaults — plaintext secret persists
|
|
375
|
+
// No verification that write succeeded
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
The chain migration approach (v1 → v2 → v3 sequentially) is deliberately chosen over direct migration because it reuses tested migration logic from each version. For users upgrading from v1.0 directly to v3.0, all three steps run. The schema version only advances after all steps succeed — a crash mid-migration leaves the version at the old number for clean retry.
|
|
381
|
+
|
|
382
|
+
---
|
|
383
|
+
|
|
384
|
+
## Orphaned Items: Why You Must Never Rename kSecAttrService
|
|
385
|
+
|
|
386
|
+
```swift
|
|
387
|
+
// ❌ INCORRECT: SecItemUpdate CANNOT change primary key attributes
|
|
388
|
+
let query: [String: Any] = [
|
|
389
|
+
kSecClass as String: kSecClassGenericPassword,
|
|
390
|
+
kSecAttrService as String: "OldServiceName",
|
|
391
|
+
kSecAttrAccount as String: "authToken"
|
|
392
|
+
]
|
|
393
|
+
let update: [String: Any] = [
|
|
394
|
+
kSecAttrService as String: "com.mycompany.myapp" // ERROR: primary key
|
|
395
|
+
]
|
|
396
|
+
// SecItemUpdate returns an error — primary keys are immutable via Update
|
|
397
|
+
SecItemUpdate(query as CFDictionary, update as CFDictionary)
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
```swift
|
|
401
|
+
// ✅ CORRECT: Full rekey migration when service name must change
|
|
402
|
+
func migrateServiceName() async throws {
|
|
403
|
+
let oldService = "OldServiceName"
|
|
404
|
+
let newService = "com.mycompany.myapp"
|
|
405
|
+
let accounts = ["authToken", "refreshToken"]
|
|
406
|
+
|
|
407
|
+
for account in accounts {
|
|
408
|
+
let oldData: Data
|
|
409
|
+
do {
|
|
410
|
+
oldData = try keychainRead(service: oldService, account: account)
|
|
411
|
+
} catch { continue } // Already migrated or never existed
|
|
412
|
+
|
|
413
|
+
try keychainSave(oldData, service: newService, account: account)
|
|
414
|
+
|
|
415
|
+
// Verify new location before deleting old
|
|
416
|
+
let verified = try keychainRead(service: newService, account: account)
|
|
417
|
+
guard verified == oldData else {
|
|
418
|
+
throw AtomicMigrator.MigrationError.verificationFailed(key: account)
|
|
419
|
+
}
|
|
420
|
+
try keychainDelete(service: oldService, account: account)
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
**Lock down your `kSecAttrService` value early and never change it.** Use your bundle identifier (e.g., `com.mycompany.myapp`) — it's unique, stable, and conventional.
|
|
426
|
+
|
|
427
|
+
---
|
|
428
|
+
|
|
429
|
+
## Background Launch and the Locked-Device Trap
|
|
430
|
+
|
|
431
|
+
iOS 15+ pre-warming and background execution (push notifications, background fetch, Live Activities) can launch your app while the device is locked. The `kSecAttrAccessible` value you choose determines whether keychain operations succeed in these contexts.
|
|
432
|
+
|
|
433
|
+
> For the complete accessibility constant selection matrix with data protection tiers and security trade-offs, see `keychain-access-control.md` § The "When" Layer: Seven Accessibility Constants. The table below summarizes the four constants most relevant to background migration scenarios.
|
|
434
|
+
|
|
435
|
+
| Accessibility constant | Available when locked | Background safe | Notes |
|
|
436
|
+
| -------------------------------------------------- | --------------------- | --------------- | ---------------------------------------------------- |
|
|
437
|
+
| `kSecAttrAccessibleWhenUnlocked` (default) | No | No | Foreground only |
|
|
438
|
+
| `kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly` | After first unlock | Yes | **Recommended** — background + device-bound |
|
|
439
|
+
| `kSecAttrAccessibleAfterFirstUnlock` | After first unlock | Yes | Background + backup migration (use only when needed) |
|
|
440
|
+
| `kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly` | No | No | Biometric-gated items |
|
|
441
|
+
| `kSecAttrAccessibleAlways` | Yes | Yes | **Deprecated iOS 12** — do not use |
|
|
442
|
+
|
|
443
|
+
**Recommended default for migrated credentials:** `kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly` — background-safe, not synced to iCloud, not included in backups. Apple uses `AfterFirstUnlock` for Wi-Fi passwords and mail account credentials.
|
|
444
|
+
|
|
445
|
+
A critical trap: **`SecItemDelete` does NOT require the item's protection-class key material** — it succeeds even when the item's data is unreadable due to lock state. This enables a devastating anti-pattern:
|
|
446
|
+
|
|
447
|
+
```swift
|
|
448
|
+
// ❌ DANGEROUS: Delete-on-read-failure destroys data during background launch
|
|
449
|
+
func dangerousTokenRefresh() {
|
|
450
|
+
var result: AnyObject?
|
|
451
|
+
let status = SecItemCopyMatching(query as CFDictionary, &result)
|
|
452
|
+
|
|
453
|
+
if status != errSecSuccess {
|
|
454
|
+
// "Can't read? Must be corrupted. Delete and start fresh."
|
|
455
|
+
SecItemDelete(query as CFDictionary) // ← DESTROYS VALID TOKEN
|
|
456
|
+
// During background launch with WhenUnlocked, the read fails
|
|
457
|
+
// with -25308 (interaction not allowed), but delete succeeds.
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// ✅ CORRECT: Distinguish "not found" from "device locked"
|
|
462
|
+
func safeTokenRead() throws -> Data? {
|
|
463
|
+
var result: AnyObject?
|
|
464
|
+
let status = SecItemCopyMatching(query as CFDictionary, &result)
|
|
465
|
+
|
|
466
|
+
switch status {
|
|
467
|
+
case errSecSuccess:
|
|
468
|
+
return result as? Data
|
|
469
|
+
case errSecItemNotFound:
|
|
470
|
+
return nil // Genuinely absent
|
|
471
|
+
case errSecInteractionNotAllowed:
|
|
472
|
+
// Device locked — item exists but unreadable right now.
|
|
473
|
+
// Do NOT delete. Do NOT treat as missing. Retry later.
|
|
474
|
+
throw KeychainError.interactionNotAllowed
|
|
475
|
+
default:
|
|
476
|
+
throw KeychainError.unexpectedStatus(status)
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
```
|
|
480
|
+
|
|
481
|
+
**Migration rule:** Always guard migration behind `UIApplication.shared.isProtectedDataAvailable`. If the device is locked, defer using `protectedDataDidBecomeAvailableNotification`. Never interpret an empty read during a locked state as "nothing to migrate."
|
|
482
|
+
|
|
483
|
+
---
|
|
484
|
+
|
|
485
|
+
## The Phantom Mismatch Bug
|
|
486
|
+
|
|
487
|
+
Including `kSecAttrAccessible` in a search query causes a "not-found then duplicate" paradox. The search filters by accessibility class, but the item was stored with a different class — so `SecItemCopyMatching` returns `errSecItemNotFound` while `SecItemAdd` sees the item via primary key and returns `errSecDuplicateItem`.
|
|
488
|
+
|
|
489
|
+
```swift
|
|
490
|
+
// ❌ INCORRECT: kSecAttrAccessible in search query causes phantom mismatches
|
|
491
|
+
let query: [String: Any] = [
|
|
492
|
+
kSecClass as String: kSecClassGenericPassword,
|
|
493
|
+
kSecAttrService as String: service,
|
|
494
|
+
kSecAttrAccount as String: account,
|
|
495
|
+
kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlocked, // ← BUG
|
|
496
|
+
kSecReturnData as String: kCFBooleanTrue as Any
|
|
497
|
+
]
|
|
498
|
+
// If stored with AfterFirstUnlock, query returns errSecItemNotFound.
|
|
499
|
+
// But SecItemAdd sees the item via primary key → errSecDuplicateItem. Deadlock.
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
**Rule:** Use **only primary key attributes** (`kSecClass`, `kSecAttrService`, `kSecAttrAccount`) in search queries. Set `kSecAttrAccessible` only during `SecItemAdd` or in the update dictionary of `SecItemUpdate`.
|
|
503
|
+
|
|
504
|
+
```swift
|
|
505
|
+
// ✅ CORRECT: search by primary key only
|
|
506
|
+
let query: [String: Any] = [
|
|
507
|
+
kSecClass as String: kSecClassGenericPassword,
|
|
508
|
+
kSecAttrService as String: service,
|
|
509
|
+
kSecAttrAccount as String: account,
|
|
510
|
+
kSecReturnData as String: kCFBooleanTrue as Any
|
|
511
|
+
]
|
|
512
|
+
```
|
|
513
|
+
|
|
514
|
+
---
|
|
515
|
+
|
|
516
|
+
## Team ID Change: The App Transfer Edge Case
|
|
517
|
+
|
|
518
|
+
When an app is transferred to a different Apple Developer account, the Team ID changes. Keychain access is permanently tied to the original Team ID — all existing keychain items become inaccessible under the new signing identity. Users are effectively logged out and lose all locally stored secrets on the first launch after updating.
|
|
519
|
+
|
|
520
|
+
**If a Team ID change is unavoidable**, you must release a "bridge" update under the **old** Team ID before the transfer:
|
|
521
|
+
|
|
522
|
+
1. Bridge update reads all keychain items and exports them to a temporary, app-group-shared container (or encrypted file in the app sandbox)
|
|
523
|
+
2. Transfer the app to the new developer account
|
|
524
|
+
3. First release under the new Team ID reads from the temporary store, writes to the new keychain, verifies, and deletes the temporary data
|
|
525
|
+
|
|
526
|
+
This is a one-way operation and must be planned well in advance. There is no way to recover keychain items after a Team ID change without the bridge update.
|
|
527
|
+
|
|
528
|
+
---
|
|
529
|
+
|
|
530
|
+
## Deferred Legacy Cleanup with Rollback Window
|
|
531
|
+
|
|
532
|
+
The safest approach keeps legacy data as backup for one release cycle after migration. Track a migration timestamp in keychain:
|
|
533
|
+
|
|
534
|
+
```swift
|
|
535
|
+
// ✅ CORRECT: Deferred cleanup with 30-day rollback window
|
|
536
|
+
actor DeferredCleanup {
|
|
537
|
+
private let cleanupDelayDays = 30
|
|
538
|
+
private let timestampAccount = "com.myapp.migration.timestamp"
|
|
539
|
+
private let serviceName = "com.myapp.credentials"
|
|
540
|
+
|
|
541
|
+
func cleanupIfExpired() async {
|
|
542
|
+
guard let data = try? keychainRead(
|
|
543
|
+
service: serviceName, account: timestampAccount),
|
|
544
|
+
let str = String(data: data, encoding: .utf8),
|
|
545
|
+
let migrationDate = ISO8601DateFormatter().date(from: str) else { return }
|
|
546
|
+
|
|
547
|
+
let days = Calendar.current.dateComponents(
|
|
548
|
+
[.day], from: migrationDate, to: Date()).day ?? 0
|
|
549
|
+
guard days >= cleanupDelayDays else { return }
|
|
550
|
+
|
|
551
|
+
// Past rollback window — safe to permanently delete legacy files
|
|
552
|
+
let documentsURL = FileManager.default.urls(
|
|
553
|
+
for: .documentDirectory, in: .userDomainMask).first!
|
|
554
|
+
for file in ["UserSession.archive", "Credentials.plist", "TokenCache.dat"] {
|
|
555
|
+
try? FileManager.default.removeItem(
|
|
556
|
+
at: documentsURL.appendingPathComponent(file))
|
|
557
|
+
}
|
|
558
|
+
if let bundleID = Bundle.main.bundleIdentifier {
|
|
559
|
+
UserDefaults.standard.removePersistentDomain(forName: bundleID)
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
```
|
|
564
|
+
|
|
565
|
+
---
|
|
566
|
+
|
|
567
|
+
## Complete App Launch Sequence
|
|
568
|
+
|
|
569
|
+
The correct ordering at app startup is critical. Keychain cleanup must happen before SDK initialization, migration must wait for protected data, and schema version gates all logic.
|
|
570
|
+
|
|
571
|
+
```swift
|
|
572
|
+
// ✅ CORRECT: Complete launch sequence with migration
|
|
573
|
+
@main
|
|
574
|
+
struct MyApp: App {
|
|
575
|
+
@UIApplicationDelegateAdaptor(AppDelegate.self) var delegate
|
|
576
|
+
var body: some Scene { WindowGroup { ContentView() } }
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
class AppDelegate: NSObject, UIApplicationDelegate {
|
|
580
|
+
func application(
|
|
581
|
+
_ application: UIApplication,
|
|
582
|
+
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
|
|
583
|
+
) -> Bool {
|
|
584
|
+
Task {
|
|
585
|
+
// 1. First-launch cleanup (stale keychain from previous install)
|
|
586
|
+
await FirstLaunchGuard.shared.performCleanupIfNeeded()
|
|
587
|
+
|
|
588
|
+
// 2. Versioned migration
|
|
589
|
+
let state = await MigrationCoordinator.shared.migrateIfNeeded()
|
|
590
|
+
switch state {
|
|
591
|
+
case .upToDate: break
|
|
592
|
+
case .migrated(let from, let to):
|
|
593
|
+
os_log(.info, "Migrated schema v%d → v%d", from, to)
|
|
594
|
+
case .deferred(let reason):
|
|
595
|
+
os_log(.info, "Migration deferred: %{public}@", reason)
|
|
596
|
+
case .failed(let error):
|
|
597
|
+
os_log(.error, "Migration failed: %{public}@",
|
|
598
|
+
error.localizedDescription)
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// 3. Deferred cleanup of legacy files past rollback window
|
|
602
|
+
await DeferredCleanup().cleanupIfExpired()
|
|
603
|
+
|
|
604
|
+
// 4. NOW initialize Firebase, analytics, auth SDKs
|
|
605
|
+
// Stale data cleared, migration complete or safely deferred
|
|
606
|
+
}
|
|
607
|
+
return true
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
```
|
|
611
|
+
|
|
612
|
+
---
|
|
613
|
+
|
|
614
|
+
## Thread Safety Note
|
|
615
|
+
|
|
616
|
+
> **Cross-validation note:** One research source claims SecItem C-APIs are non-thread-safe and recommends a serial `DispatchQueue`. Apple's documentation and Quinn "The Eskimo" (DTS) confirm that **SecItem\* functions are thread-safe on iOS**. However, your wrapper's mutable state (caches, migration flags, version tracking) does need synchronization. An `actor` provides this naturally in modern Swift concurrency — prefer actors over serial queues for new code (iOS 15+).
|
|
617
|
+
|
|
618
|
+
---
|
|
619
|
+
|
|
620
|
+
## Testing Migration Paths
|
|
621
|
+
|
|
622
|
+
Keychain behavior differs between Simulator and real devices:
|
|
623
|
+
|
|
624
|
+
| Aspect | Simulator | Real device |
|
|
625
|
+
| ----------------------------- | ------------------------ | -------------------------------------- |
|
|
626
|
+
| Data Protection enforcement | Not enforced | Fully enforced (hardware) |
|
|
627
|
+
| Keychain entitlements | Loosely enforced | Strictly enforced |
|
|
628
|
+
| `errSecInteractionNotAllowed` | Rarely triggered | Triggered when locked |
|
|
629
|
+
| Lock state testing | Cannot meaningfully test | Essential for accessibility validation |
|
|
630
|
+
|
|
631
|
+
Use **protocol-based abstraction** for unit tests (runs in CI on simulators) and real-device integration tests for accessibility-class validation:
|
|
632
|
+
|
|
633
|
+
```swift
|
|
634
|
+
// ✅ Protocol-based keychain abstraction for testable migrations
|
|
635
|
+
protocol MigrationKeychainProtocol: Actor {
|
|
636
|
+
func save(_ data: Data, service: String, account: String,
|
|
637
|
+
accessible: CFString) throws
|
|
638
|
+
func read(service: String, account: String) throws -> Data
|
|
639
|
+
func delete(service: String, account: String) throws
|
|
640
|
+
func deleteAll()
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
// In-memory mock for unit tests
|
|
644
|
+
actor MockMigrationKeychain: MigrationKeychainProtocol {
|
|
645
|
+
var store: [String: [String: Data]] = [:]
|
|
646
|
+
var simulatedError: KeychainError?
|
|
647
|
+
|
|
648
|
+
func save(_ data: Data, service: String, account: String,
|
|
649
|
+
accessible: CFString) throws {
|
|
650
|
+
if let error = simulatedError { throw error }
|
|
651
|
+
store[service, default: [:]][account] = data
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
func read(service: String, account: String) throws -> Data {
|
|
655
|
+
if let error = simulatedError { throw error }
|
|
656
|
+
guard let data = store[service]?[account] else {
|
|
657
|
+
throw KeychainError.itemNotFound
|
|
658
|
+
}
|
|
659
|
+
return data
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
func delete(service: String, account: String) throws {
|
|
663
|
+
store[service]?[account] = nil
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
func deleteAll() { store.removeAll() }
|
|
667
|
+
}
|
|
668
|
+
```
|
|
669
|
+
|
|
670
|
+
```swift
|
|
671
|
+
// ✅ Example: verify atomic behavior — legacy data preserved on failure
|
|
672
|
+
@Test func migrationPreservesLegacyDataOnKeychainFailure() async {
|
|
673
|
+
let mock = MockMigrationKeychain()
|
|
674
|
+
mock.simulatedError = .unexpectedStatus(-25308) // Simulate locked device
|
|
675
|
+
|
|
676
|
+
let defaults = UserDefaults(suiteName: "test")!
|
|
677
|
+
defaults.set("secret-token", forKey: "authToken")
|
|
678
|
+
|
|
679
|
+
let migrator = AtomicMigrator(keychain: mock)
|
|
680
|
+
let results = await migrator.migrateUserDefaultsKeys(
|
|
681
|
+
["authToken"], service: "com.myapp"
|
|
682
|
+
)
|
|
683
|
+
|
|
684
|
+
#expect(results.contains(where: { !$0.succeeded }))
|
|
685
|
+
#expect(defaults.string(forKey: "authToken") == "secret-token") // Still intact
|
|
686
|
+
}
|
|
687
|
+
```
|
|
688
|
+
|
|
689
|
+
Always clean up keychain items in `setUp()`/`tearDown()` — items persist between test runs on the same simulator. For integration tests hitting real keychain, create a Test Host app target with the Keychain capability enabled.
|
|
690
|
+
|
|
691
|
+
---
|
|
692
|
+
|
|
693
|
+
## Handling Very Old Versions and Collapse Strategy
|
|
694
|
+
|
|
695
|
+
The App Store always delivers the latest binary — a user jumping from v1.0 to v3.0 never installs v2.0. Your v3.0 binary must contain migration logic for every historical schema version.
|
|
696
|
+
|
|
697
|
+
Pragmatically, after sufficient time (when analytics show <1% of users on legacy versions), **collapse old migrations into a single mega-migration** from v0 to current, reducing code maintenance. For users on versions so old that the legacy format is unknown or corrupted, the migration should **fail gracefully** and prompt a fresh login rather than crashing.
|
|
698
|
+
|
|
699
|
+
---
|
|
700
|
+
|
|
701
|
+
## Secure Deletion: Trust Cryptographic Erasure
|
|
702
|
+
|
|
703
|
+
Do **not** attempt to manually overwrite files with zeros or random bytes before deletion — NAND flash wear-leveling makes this ineffective and wastes write cycles. iOS handles secure deletion through cryptographic erasure: every file has a per-file AES-256 key, and when the file is deleted via standard APIs (`FileManager.removeItem`, `UserDefaults.removeObject`), iOS destroys the per-file key through Effaceable Storage, rendering the physical bits permanently unrecoverable.
|
|
704
|
+
|
|
705
|
+
Standard deletion APIs are sufficient. The residual risk is unencrypted backups created _before_ migration — encourage users to use encrypted backups, and delete legacy data promptly after verified migration.
|
|
706
|
+
|
|
707
|
+
---
|
|
708
|
+
|
|
709
|
+
## Conclusion
|
|
710
|
+
|
|
711
|
+
The core insight of safe keychain migration: **deletion is the irreversible step, not the write**. Every pattern in this file follows from that principle — verify before deleting, defer when uncertain, and treat keychain persistence across reinstalls as a feature to plan for rather than a bug to fight. The five most impactful decisions are: using `kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly` for background-safe encrypted storage, implementing first-launch cleanup before SDK initialization, storing schema versions in keychain rather than UserDefaults, gating all migration behind `isProtectedDataAvailable`, and never changing `kSecAttrService` after shipping.
|
|
712
|
+
|
|
713
|
+
---
|
|
714
|
+
|
|
715
|
+
## Summary Checklist
|
|
716
|
+
|
|
717
|
+
1. **First-launch cleanup runs before any SDK initialization** — uses UserDefaults flag to detect reinstall, wipes stale keychain items, includes `kSecAttrSynchronizableAny` to catch iCloud-synced items
|
|
718
|
+
2. **Migration is atomic: read → write → verify → delete** — legacy data is never deleted until keychain write is confirmed by read-back; failed keys remain intact for retry
|
|
719
|
+
3. **Schema version stored in keychain, not UserDefaults** — survives app reinstall; version only advances after all migration steps succeed
|
|
720
|
+
4. **Protected data availability checked before any migration** — guards against iOS 15+ pre-warming and locked-device scenarios; defers via `protectedDataDidBecomeAvailableNotification`
|
|
721
|
+
5. **`errSecInteractionNotAllowed` (-25308) is never treated as "item missing"** — distinguishes locked-device failures from genuine absence; never deletes on read failure without checking status code
|
|
722
|
+
6. **`kSecAttrService` and `kSecAttrAccount` are immutable after shipping** — changing either orphans existing items; `SecItemUpdate` cannot modify primary keys; use full rekey migration if change is unavoidable
|
|
723
|
+
7. **`kSecAttrAccessible` is never included in search queries** — causes phantom "not-found then duplicate" mismatches; set only during add or in update dictionary
|
|
724
|
+
8. **Default accessibility is `AfterFirstUnlockThisDeviceOnly`** — background-safe, not synced, not backed up; matches Apple's own credential storage patterns
|
|
725
|
+
9. **Deferred legacy cleanup with rollback window** — keep legacy data for 30 days post-migration as safety net; timestamp stored in keychain
|
|
726
|
+
10. **Team ID changes sever all keychain access** — must release bridge update under old Team ID before app transfer; no recovery possible after transfer without bridge
|
|
727
|
+
11. **Migration tested via protocol-based abstraction** — mock keychain in unit tests; real-device integration tests for accessibility class validation; clean up items in setUp/tearDown
|