@mmerterden/multi-agent-pipeline 20.7.0 → 20.8.1

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.
Files changed (284) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/LICENSE +0 -10
  3. package/docs/facts.json +1 -1
  4. package/manifest.json +285 -285
  5. package/package.json +3 -3
  6. package/pipeline/lib/redact.mjs +3 -2
  7. package/pipeline/scripts/_notices.mjs +1 -1
  8. package/pipeline/scripts/gen-skills-index.mjs +13 -1
  9. package/pipeline/scripts/pre-commit-check.sh +4 -0
  10. package/pipeline/skills/.skill-manifest.json +69 -69
  11. package/pipeline/skills/shared/README.md +70 -70
  12. package/pipeline/skills/shared/external/alarmkit/SKILL.md +373 -381
  13. package/pipeline/skills/shared/external/alarmkit/evals/evals.json +23 -18
  14. package/pipeline/skills/shared/external/alarmkit/references/alarmkit-patterns.md +328 -378
  15. package/pipeline/skills/shared/external/app-clips/SKILL.md +260 -160
  16. package/pipeline/skills/shared/external/app-clips/evals/evals.json +27 -27
  17. package/pipeline/skills/shared/external/app-clips/references/data-handoff-notifications-location.md +150 -83
  18. package/pipeline/skills/shared/external/app-clips/references/routing-and-experiences.md +135 -83
  19. package/pipeline/skills/shared/external/app-clips/references/size-capabilities-and-promotion.md +143 -85
  20. package/pipeline/skills/shared/external/app-intents/SKILL.md +302 -304
  21. package/pipeline/skills/shared/external/app-intents/evals/evals.json +21 -21
  22. package/pipeline/skills/shared/external/app-intents/references/appintents-advanced.md +594 -894
  23. package/pipeline/skills/shared/external/app-store-optimization/SKILL.md +345 -277
  24. package/pipeline/skills/shared/external/app-store-optimization/evals/evals.json +27 -23
  25. package/pipeline/skills/shared/external/app-store-optimization/references/keyword-research-methodology.md +107 -121
  26. package/pipeline/skills/shared/external/app-store-optimization/references/product-page-variants.md +145 -165
  27. package/pipeline/skills/shared/external/app-store-review/SKILL.md +306 -326
  28. package/pipeline/skills/shared/external/app-store-review/evals/evals.json +21 -21
  29. package/pipeline/skills/shared/external/app-store-review/references/privacy-manifest.md +105 -67
  30. package/pipeline/skills/shared/external/app-store-review/references/review-checklists.md +114 -101
  31. package/pipeline/skills/shared/external/apple-on-device-ai/SKILL.md +335 -360
  32. package/pipeline/skills/shared/external/apple-on-device-ai/evals/evals.json +24 -27
  33. package/pipeline/skills/shared/external/apple-on-device-ai/references/coreml-conversion.md +215 -322
  34. package/pipeline/skills/shared/external/apple-on-device-ai/references/coreml-optimization.md +161 -256
  35. package/pipeline/skills/shared/external/apple-on-device-ai/references/foundation-models.md +277 -387
  36. package/pipeline/skills/shared/external/apple-on-device-ai/references/mlx-swift.md +196 -210
  37. package/pipeline/skills/shared/external/authentication/SKILL.md +277 -381
  38. package/pipeline/skills/shared/external/authentication/evals/evals.json +25 -25
  39. package/pipeline/skills/shared/external/authentication/references/keychain-biometric.md +135 -178
  40. package/pipeline/skills/shared/external/authentication/references/passkeys.md +111 -147
  41. package/pipeline/skills/shared/external/avkit/SKILL.md +267 -364
  42. package/pipeline/skills/shared/external/avkit/evals/evals.json +26 -26
  43. package/pipeline/skills/shared/external/avkit/references/avkit-patterns.md +375 -493
  44. package/pipeline/skills/shared/external/background-processing/SKILL.md +274 -384
  45. package/pipeline/skills/shared/external/background-processing/evals/evals.json +22 -22
  46. package/pipeline/skills/shared/external/background-processing/references/background-task-patterns.md +173 -321
  47. package/pipeline/skills/shared/external/callkit-voip/SKILL.md +290 -371
  48. package/pipeline/skills/shared/external/callkit-voip/evals/evals.json +24 -24
  49. package/pipeline/skills/shared/external/callkit-voip/references/callkit-patterns.md +175 -343
  50. package/pipeline/skills/shared/external/cloudkit-sync/SKILL.md +292 -381
  51. package/pipeline/skills/shared/external/cloudkit-sync/evals/evals.json +33 -30
  52. package/pipeline/skills/shared/external/cloudkit-sync/references/cloudkit-patterns.md +227 -355
  53. package/pipeline/skills/shared/external/contacts-framework/SKILL.md +197 -346
  54. package/pipeline/skills/shared/external/contacts-framework/evals/evals.json +19 -21
  55. package/pipeline/skills/shared/external/contacts-framework/references/contacts-patterns.md +169 -308
  56. package/pipeline/skills/shared/external/core-bluetooth/SKILL.md +228 -376
  57. package/pipeline/skills/shared/external/core-bluetooth/evals/evals.json +25 -22
  58. package/pipeline/skills/shared/external/core-bluetooth/references/ble-patterns.md +257 -337
  59. package/pipeline/skills/shared/external/core-data/SKILL.md +302 -368
  60. package/pipeline/skills/shared/external/core-data/evals/evals.json +30 -27
  61. package/pipeline/skills/shared/external/core-motion/SKILL.md +235 -324
  62. package/pipeline/skills/shared/external/core-motion/evals/evals.json +31 -27
  63. package/pipeline/skills/shared/external/core-motion/references/motion-patterns.md +210 -310
  64. package/pipeline/skills/shared/external/core-nfc/SKILL.md +292 -366
  65. package/pipeline/skills/shared/external/core-nfc/evals/evals.json +22 -24
  66. package/pipeline/skills/shared/external/core-nfc/references/nfc-patterns.md +233 -329
  67. package/pipeline/skills/shared/external/coreml/SKILL.md +323 -367
  68. package/pipeline/skills/shared/external/coreml/evals/evals.json +24 -21
  69. package/pipeline/skills/shared/external/coreml/references/coreml-swift-integration.md +562 -565
  70. package/pipeline/skills/shared/external/cryptokit/SKILL.md +253 -394
  71. package/pipeline/skills/shared/external/cryptokit/evals/evals.json +20 -18
  72. package/pipeline/skills/shared/external/cryptokit/references/cryptokit-patterns.md +299 -488
  73. package/pipeline/skills/shared/external/debugging-instruments/SKILL.md +270 -323
  74. package/pipeline/skills/shared/external/debugging-instruments/evals/evals.json +27 -30
  75. package/pipeline/skills/shared/external/debugging-instruments/references/instruments-guide.md +167 -315
  76. package/pipeline/skills/shared/external/debugging-instruments/references/lldb-patterns.md +140 -193
  77. package/pipeline/skills/shared/external/device-integrity/SKILL.md +236 -353
  78. package/pipeline/skills/shared/external/device-integrity/evals/evals.json +25 -21
  79. package/pipeline/skills/shared/external/device-integrity/references/device-integrity-patterns.md +159 -197
  80. package/pipeline/skills/shared/external/energykit/SKILL.md +225 -392
  81. package/pipeline/skills/shared/external/energykit/evals/evals.json +29 -28
  82. package/pipeline/skills/shared/external/energykit/references/energykit-patterns.md +174 -470
  83. package/pipeline/skills/shared/external/eventkit-calendar/SKILL.md +261 -383
  84. package/pipeline/skills/shared/external/eventkit-calendar/evals/evals.json +25 -22
  85. package/pipeline/skills/shared/external/eventkit-calendar/references/eventkit-patterns.md +165 -268
  86. package/pipeline/skills/shared/external/healthkit/SKILL.md +252 -303
  87. package/pipeline/skills/shared/external/healthkit/evals/evals.json +24 -23
  88. package/pipeline/skills/shared/external/healthkit/references/healthkit-patterns.md +369 -523
  89. package/pipeline/skills/shared/external/homekit-matter/SKILL.md +233 -348
  90. package/pipeline/skills/shared/external/homekit-matter/evals/evals.json +27 -22
  91. package/pipeline/skills/shared/external/homekit-matter/references/matter-commissioning.md +199 -305
  92. package/pipeline/skills/shared/external/ios-accessibility/SKILL.md +368 -340
  93. package/pipeline/skills/shared/external/ios-accessibility/evals/evals.json +28 -27
  94. package/pipeline/skills/shared/external/ios-accessibility/references/a11y-patterns.md +314 -260
  95. package/pipeline/skills/shared/external/ios-accessibility/references/media-accessibility.md +97 -67
  96. package/pipeline/skills/shared/external/ios-accessibility/references/nutrition-labels.md +165 -101
  97. package/pipeline/skills/shared/external/ios-localization/SKILL.md +258 -371
  98. package/pipeline/skills/shared/external/ios-localization/evals/evals.json +23 -23
  99. package/pipeline/skills/shared/external/ios-localization/references/formatstyle-locale.md +283 -491
  100. package/pipeline/skills/shared/external/ios-localization/references/string-catalogs.md +313 -440
  101. package/pipeline/skills/shared/external/ios-networking/SKILL.md +265 -341
  102. package/pipeline/skills/shared/external/ios-networking/evals/evals.json +24 -24
  103. package/pipeline/skills/shared/external/ios-networking/references/background-websocket.md +425 -652
  104. package/pipeline/skills/shared/external/ios-networking/references/file-storage-patterns.md +143 -285
  105. package/pipeline/skills/shared/external/ios-networking/references/lightweight-clients.md +93 -53
  106. package/pipeline/skills/shared/external/ios-networking/references/network-framework.md +231 -456
  107. package/pipeline/skills/shared/external/ios-networking/references/urlsession-patterns.md +517 -784
  108. package/pipeline/skills/shared/external/ios-simulator/SKILL.md +265 -393
  109. package/pipeline/skills/shared/external/ios-simulator/evals/evals.json +21 -21
  110. package/pipeline/skills/shared/external/ios-simulator/references/simctl-commands.md +177 -270
  111. package/pipeline/skills/shared/external/live-activities/SKILL.md +318 -360
  112. package/pipeline/skills/shared/external/live-activities/evals/evals.json +21 -21
  113. package/pipeline/skills/shared/external/live-activities/references/activitykit-patterns.md +478 -710
  114. package/pipeline/skills/shared/external/localization-reuse-map/SKILL.md +3 -3
  115. package/pipeline/skills/shared/external/localization-reuse-map/example-mapping.json +1 -1
  116. package/pipeline/skills/shared/external/localization-reuse-map/reference/format-and-output.md +8 -7
  117. package/pipeline/skills/shared/external/localization-reuse-map/reference/publish-and-snapshot.md +2 -2
  118. package/pipeline/skills/shared/external/localization-reuse-map/reference/sources-and-recipes.md +5 -2
  119. package/pipeline/skills/shared/external/localization-reuse-map/scripts/_shared.py +100 -0
  120. package/pipeline/skills/shared/external/localization-reuse-map/scripts/build-artifact.py +45 -26
  121. package/pipeline/skills/shared/external/localization-reuse-map/scripts/build-spreadsheet.py +14 -16
  122. package/pipeline/skills/shared/external/localization-reuse-map/scripts/fetch-annotations.py +12 -5
  123. package/pipeline/skills/shared/external/localization-reuse-map/scripts/fetch-legacy-labels.py +2 -1
  124. package/pipeline/skills/shared/external/localization-reuse-map/scripts/publish-confluence.py +6 -5
  125. package/pipeline/skills/shared/external/localization-reuse-map/scripts/render-overlay.py +44 -18
  126. package/pipeline/skills/shared/external/localization-reuse-map/scripts/resolve-legacy-values.py +5 -2
  127. package/pipeline/skills/shared/external/localization-reuse-map/scripts/resolve-new-values.py +10 -11
  128. package/pipeline/skills/shared/external/localization-reuse-map/scripts/scan-screen-keys.py +4 -33
  129. package/pipeline/skills/shared/external/localization-reuse-map/scripts/verify-map.py +12 -59
  130. package/pipeline/skills/shared/external/mapkit-location/SKILL.md +297 -267
  131. package/pipeline/skills/shared/external/mapkit-location/evals/evals.json +28 -24
  132. package/pipeline/skills/shared/external/mapkit-location/references/mapkit-corelocation-patterns.md +378 -532
  133. package/pipeline/skills/shared/external/mapkit-location/references/mapkit-patterns.md +397 -499
  134. package/pipeline/skills/shared/external/metrickit-diagnostics/SKILL.md +165 -348
  135. package/pipeline/skills/shared/external/metrickit-diagnostics/evals/evals.json +26 -23
  136. package/pipeline/skills/shared/external/metrickit-diagnostics/references/metrickit-patterns.md +123 -130
  137. package/pipeline/skills/shared/external/musickit-audio/SKILL.md +189 -315
  138. package/pipeline/skills/shared/external/musickit-audio/evals/evals.json +22 -21
  139. package/pipeline/skills/shared/external/musickit-audio/references/musickit-patterns.md +181 -270
  140. package/pipeline/skills/shared/external/natural-language/SKILL.md +188 -340
  141. package/pipeline/skills/shared/external/natural-language/evals/evals.json +21 -21
  142. package/pipeline/skills/shared/external/natural-language/references/translation-patterns.md +171 -225
  143. package/pipeline/skills/shared/external/passkit-wallet/SKILL.md +258 -392
  144. package/pipeline/skills/shared/external/passkit-wallet/evals/evals.json +30 -29
  145. package/pipeline/skills/shared/external/passkit-wallet/references/wallet-passes.md +164 -231
  146. package/pipeline/skills/shared/external/pdfkit/SKILL.md +312 -344
  147. package/pipeline/skills/shared/external/pdfkit/evals/evals.json +19 -19
  148. package/pipeline/skills/shared/external/pdfkit/references/pdfkit-patterns.md +413 -624
  149. package/pipeline/skills/shared/external/pencilkit-drawing/SKILL.md +242 -358
  150. package/pipeline/skills/shared/external/pencilkit-drawing/evals/evals.json +25 -21
  151. package/pipeline/skills/shared/external/pencilkit-drawing/references/pencilkit-patterns.md +161 -226
  152. package/pipeline/skills/shared/external/permissionkit/SKILL.md +282 -400
  153. package/pipeline/skills/shared/external/permissionkit/evals/evals.json +27 -30
  154. package/pipeline/skills/shared/external/permissionkit/references/permissionkit-patterns.md +237 -350
  155. package/pipeline/skills/shared/external/photos-camera-media/SKILL.md +276 -325
  156. package/pipeline/skills/shared/external/photos-camera-media/references/av-playback.md +299 -545
  157. package/pipeline/skills/shared/external/photos-camera-media/references/camera-capture.md +344 -588
  158. package/pipeline/skills/shared/external/photos-camera-media/references/image-loading-caching.md +316 -660
  159. package/pipeline/skills/shared/external/photos-camera-media/references/photokit-patterns.md +270 -416
  160. package/pipeline/skills/shared/external/push-notifications/SKILL.md +312 -340
  161. package/pipeline/skills/shared/external/push-notifications/evals/evals.json +27 -26
  162. package/pipeline/skills/shared/external/push-notifications/references/notification-patterns.md +328 -485
  163. package/pipeline/skills/shared/external/push-notifications/references/rich-notifications.md +327 -560
  164. package/pipeline/skills/shared/external/realitykit-ar/SKILL.md +218 -410
  165. package/pipeline/skills/shared/external/realitykit-ar/evals/evals.json +24 -27
  166. package/pipeline/skills/shared/external/realitykit-ar/references/realitykit-patterns.md +221 -348
  167. package/pipeline/skills/shared/external/shareplay-activities/SKILL.md +222 -393
  168. package/pipeline/skills/shared/external/shareplay-activities/evals/evals.json +23 -24
  169. package/pipeline/skills/shared/external/shareplay-activities/references/shareplay-patterns.md +280 -420
  170. package/pipeline/skills/shared/external/skill-creator/template.md +7 -1
  171. package/pipeline/skills/shared/external/speech-recognition/SKILL.md +217 -421
  172. package/pipeline/skills/shared/external/speech-recognition/evals/evals.json +23 -26
  173. package/pipeline/skills/shared/external/speech-recognition/references/speechanalyzer-patterns.md +133 -125
  174. package/pipeline/skills/shared/external/storekit/SKILL.md +228 -204
  175. package/pipeline/skills/shared/external/storekit/evals/evals.json +27 -24
  176. package/pipeline/skills/shared/external/storekit/references/app-review-guidelines.md +98 -109
  177. package/pipeline/skills/shared/external/storekit/references/core-patterns.md +302 -241
  178. package/pipeline/skills/shared/external/storekit/references/storekit-advanced.md +356 -649
  179. package/pipeline/skills/shared/external/swift-api-design-guidelines/SKILL.md +274 -399
  180. package/pipeline/skills/shared/external/swift-api-design-guidelines/evals/evals.json +22 -24
  181. package/pipeline/skills/shared/external/swift-api-design-guidelines/references/argument-labels-and-parameters.md +107 -108
  182. package/pipeline/skills/shared/external/swift-api-design-guidelines/references/conventions-and-special-rules.md +93 -165
  183. package/pipeline/skills/shared/external/swift-api-design-guidelines/references/naming-and-clarity.md +99 -137
  184. package/pipeline/skills/shared/external/swift-api-design-guidelines/references/side-effects-and-mutating-pairs.md +77 -120
  185. package/pipeline/skills/shared/external/swift-architecture/SKILL.md +334 -350
  186. package/pipeline/skills/shared/external/swift-architecture/evals/evals.json +22 -22
  187. package/pipeline/skills/shared/external/swift-charts/SKILL.md +208 -394
  188. package/pipeline/skills/shared/external/swift-charts/evals/evals.json +27 -30
  189. package/pipeline/skills/shared/external/swift-charts/references/charts-patterns.md +351 -762
  190. package/pipeline/skills/shared/external/swift-codable/SKILL.md +339 -343
  191. package/pipeline/skills/shared/external/swift-codable/evals/evals.json +20 -20
  192. package/pipeline/skills/shared/external/swift-concurrency/SKILL.md +304 -351
  193. package/pipeline/skills/shared/external/swift-concurrency/evals/evals.json +27 -24
  194. package/pipeline/skills/shared/external/swift-concurrency/references/approachable-concurrency.md +65 -80
  195. package/pipeline/skills/shared/external/swift-concurrency/references/async-algorithms.md +48 -84
  196. package/pipeline/skills/shared/external/swift-concurrency/references/bridging-interop.md +134 -79
  197. package/pipeline/skills/shared/external/swift-concurrency/references/concurrency-patterns.md +145 -167
  198. package/pipeline/skills/shared/external/swift-concurrency/references/diagnostics.md +62 -50
  199. package/pipeline/skills/shared/external/swift-concurrency/references/swiftui-concurrency.md +92 -121
  200. package/pipeline/skills/shared/external/swift-concurrency/references/synchronization-primitives.md +177 -241
  201. package/pipeline/skills/shared/external/swift-formatstyle/SKILL.md +258 -234
  202. package/pipeline/skills/shared/external/swift-language/SKILL.md +342 -382
  203. package/pipeline/skills/shared/external/swift-language/evals/evals.json +24 -27
  204. package/pipeline/skills/shared/external/swift-language/references/swift-attributes-interop.md +79 -56
  205. package/pipeline/skills/shared/external/swift-language/references/swift-patterns-extended.md +297 -340
  206. package/pipeline/skills/shared/external/swift-security/SKILL.md +183 -162
  207. package/pipeline/skills/shared/external/swift-security/evals/evals.json +25 -25
  208. package/pipeline/skills/shared/external/swift-security/references/biometric-authentication.md +314 -469
  209. package/pipeline/skills/shared/external/swift-security/references/certificate-trust.md +411 -476
  210. package/pipeline/skills/shared/external/swift-security/references/common-anti-patterns.md +260 -530
  211. package/pipeline/skills/shared/external/swift-security/references/compliance-owasp-mapping.md +270 -477
  212. package/pipeline/skills/shared/external/swift-security/references/credential-storage-patterns.md +573 -571
  213. package/pipeline/skills/shared/external/swift-security/references/cryptokit-public-key.md +370 -441
  214. package/pipeline/skills/shared/external/swift-security/references/cryptokit-symmetric.md +332 -433
  215. package/pipeline/skills/shared/external/swift-security/references/keychain-access-control.md +346 -468
  216. package/pipeline/skills/shared/external/swift-security/references/keychain-fundamentals.md +375 -491
  217. package/pipeline/skills/shared/external/swift-security/references/keychain-item-classes.md +431 -432
  218. package/pipeline/skills/shared/external/swift-security/references/keychain-sharing.md +328 -425
  219. package/pipeline/skills/shared/external/swift-security/references/migration-legacy-stores.md +341 -579
  220. package/pipeline/skills/shared/external/swift-security/references/secure-enclave.md +397 -457
  221. package/pipeline/skills/shared/external/swift-security/references/testing-security-code.md +354 -614
  222. package/pipeline/skills/shared/external/swift-testing/SKILL.md +191 -175
  223. package/pipeline/skills/shared/external/swift-testing/evals/evals.json +26 -24
  224. package/pipeline/skills/shared/external/swift-testing/references/testing-advanced.md +81 -84
  225. package/pipeline/skills/shared/external/swift-testing/references/testing-patterns.md +317 -433
  226. package/pipeline/skills/shared/external/swiftdata/SKILL.md +394 -256
  227. package/pipeline/skills/shared/external/swiftdata/evals/evals.json +24 -24
  228. package/pipeline/skills/shared/external/swiftdata/references/core-data-coexistence.md +206 -402
  229. package/pipeline/skills/shared/external/swiftdata/references/indexing.md +59 -52
  230. package/pipeline/skills/shared/external/swiftdata/references/predicate-pitfalls.md +57 -33
  231. package/pipeline/skills/shared/external/swiftdata/references/swiftdata-advanced.md +354 -747
  232. package/pipeline/skills/shared/external/swiftdata/references/swiftdata-queries.md +300 -508
  233. package/pipeline/skills/shared/external/swiftlint/SKILL.md +175 -226
  234. package/pipeline/skills/shared/external/swiftlint/references/adoption-and-configuration.md +141 -208
  235. package/pipeline/skills/shared/external/swiftlint/references/custom-rules-and-analyze.md +100 -109
  236. package/pipeline/skills/shared/external/swiftlint/references/plugins-run-scripts-and-integrations.md +159 -179
  237. package/pipeline/skills/shared/external/swiftlint/references/rule-reference.md +383 -18
  238. package/pipeline/skills/shared/external/swiftlint/references/rules-suppressions-and-baselines.md +143 -229
  239. package/pipeline/skills/shared/external/swiftui-animation/SKILL.md +283 -366
  240. package/pipeline/skills/shared/external/swiftui-animation/references/animation-advanced.md +396 -608
  241. package/pipeline/skills/shared/external/swiftui-animation/references/core-animation-bridge.md +336 -385
  242. package/pipeline/skills/shared/external/swiftui-gestures/SKILL.md +239 -349
  243. package/pipeline/skills/shared/external/swiftui-gestures/references/gesture-patterns.md +228 -310
  244. package/pipeline/skills/shared/external/swiftui-layout-components/SKILL.md +260 -249
  245. package/pipeline/skills/shared/external/swiftui-layout-components/references/form.md +92 -74
  246. package/pipeline/skills/shared/external/swiftui-layout-components/references/grids.md +112 -177
  247. package/pipeline/skills/shared/external/swiftui-layout-components/references/list.md +61 -64
  248. package/pipeline/skills/shared/external/swiftui-layout-components/references/scrollview.md +94 -134
  249. package/pipeline/skills/shared/external/swiftui-liquid-glass/SKILL.md +193 -225
  250. package/pipeline/skills/shared/external/swiftui-liquid-glass/references/liquid-glass.md +173 -327
  251. package/pipeline/skills/shared/external/swiftui-navigation/SKILL.md +201 -168
  252. package/pipeline/skills/shared/external/swiftui-navigation/references/deeplinks.md +127 -150
  253. package/pipeline/skills/shared/external/swiftui-navigation/references/navigationstack.md +134 -133
  254. package/pipeline/skills/shared/external/swiftui-navigation/references/sheets.md +152 -117
  255. package/pipeline/skills/shared/external/swiftui-navigation/references/tabview.md +111 -138
  256. package/pipeline/skills/shared/external/swiftui-patterns/SKILL.md +316 -252
  257. package/pipeline/skills/shared/external/swiftui-patterns/references/architecture-patterns.md +341 -332
  258. package/pipeline/skills/shared/external/swiftui-patterns/references/deprecated-migration.md +547 -854
  259. package/pipeline/skills/shared/external/swiftui-patterns/references/design-polish.md +485 -537
  260. package/pipeline/skills/shared/external/swiftui-patterns/references/platform-and-sharing.md +417 -499
  261. package/pipeline/skills/shared/external/swiftui-performance/SKILL.md +213 -376
  262. package/pipeline/skills/shared/external/swiftui-performance/references/demystify-swiftui-performance-wwdc23.md +86 -175
  263. package/pipeline/skills/shared/external/swiftui-performance/references/optimizing-swiftui-performance-instruments.md +89 -195
  264. package/pipeline/skills/shared/external/swiftui-performance/references/understanding-hangs-in-your-app.md +95 -182
  265. package/pipeline/skills/shared/external/swiftui-performance/references/understanding-improving-swiftui-performance.md +71 -149
  266. package/pipeline/skills/shared/external/swiftui-performance/references/wwdc-session-sources.md +21 -27
  267. package/pipeline/skills/shared/external/swiftui-uikit-interop/SKILL.md +303 -295
  268. package/pipeline/skills/shared/external/swiftui-uikit-interop/references/hosting-migration.md +204 -387
  269. package/pipeline/skills/shared/external/swiftui-uikit-interop/references/representable-recipes.md +469 -683
  270. package/pipeline/skills/shared/external/swiftui-webkit/SKILL.md +140 -186
  271. package/pipeline/skills/shared/external/swiftui-webkit/references/loading-and-observation.md +75 -86
  272. package/pipeline/skills/shared/external/swiftui-webkit/references/local-content-and-custom-schemes.md +63 -60
  273. package/pipeline/skills/shared/external/swiftui-webkit/references/migration-and-fallbacks.md +69 -137
  274. package/pipeline/skills/shared/external/swiftui-webkit/references/navigation-and-javascript.md +95 -67
  275. package/pipeline/skills/shared/external/tipkit/SKILL.md +220 -335
  276. package/pipeline/skills/shared/external/tipkit/references/tipkit-patterns.md +356 -494
  277. package/pipeline/skills/shared/external/vision-framework/SKILL.md +260 -375
  278. package/pipeline/skills/shared/external/vision-framework/references/vision-requests.md +393 -515
  279. package/pipeline/skills/shared/external/vision-framework/references/visionkit-scanner.md +363 -539
  280. package/pipeline/skills/shared/external/weatherkit/SKILL.md +160 -315
  281. package/pipeline/skills/shared/external/weatherkit/references/weatherkit-patterns.md +288 -407
  282. package/pipeline/skills/shared/external/widgetkit/SKILL.md +224 -288
  283. package/pipeline/skills/shared/external/widgetkit/references/widgetkit-advanced.md +416 -719
  284. package/pipeline/skills/shared/external/NOTICE-swift-ios-skills.md +0 -39
@@ -1,620 +1,504 @@
1
1
  # Keychain Fundamentals
2
2
 
3
- > **Scope:** SecItem\* CRUD operations, query dictionary structure, kSecClass types, OSStatus error handling, actor-based wrapper patterns. This is the foundation file - all other reference files assume familiarity with these patterns.
4
- >
5
- > **Key APIs:** `SecItemAdd`, `SecItemCopyMatching`, `SecItemUpdate`, `SecItemDelete`, `kSecClassGenericPassword`, `kSecClassInternetPassword`, `kSecClassKey`, `kSecClassCertificate`, `kSecClassIdentity`
6
- >
7
- > **Apple Documentation:** [Keychain Services](https://sosumi.ai/documentation/security/keychain_services), [TN3137](https://sosumi.ai/documentation/technotes/tn3137-on-mac-keychains), Quinn "The Eskimo!" DTS posts: "SecItem: Fundamentals" and "SecItem: Pitfalls and Best Practices"
3
+ Every other reference in this skill builds on the material here: how the four
4
+ `SecItem` functions behave, what their dictionaries may contain, how to read
5
+ their status codes, and how to keep them away from the main thread.
8
6
 
9
- ---
7
+ Primary sources: TN3137 ("On Mac keychain APIs and implementations"), the
8
+ Keychain Services pages in Apple's documentation, and the Apple Developer Forums posts by
9
+ Quinn "The Eskimo!" (DTS) titled "SecItem: Fundamentals" and "SecItem: Pitfalls
10
+ and Best Practices".
10
11
 
11
12
  ## Contents
12
13
 
13
- - [Architecture Overview](#architecture-overview)
14
- - [The Four Functions and Their Dictionary Contracts](#the-four-functions-and-their-dictionary-contracts)
15
- - [Uniqueness and Primary Keys](#uniqueness-and-primary-keys)
16
- - [The Add-or-Update Pattern](#the-add-or-update-pattern)
17
- - [Reading from the Keychain: Return Flags and Type Casting](#reading-from-the-keychain-return-flags-and-type-casting)
18
- - [Return Type Cheat Sheet](#return-type-cheat-sheet)
19
- - [String Keys vs. kSec\* Constants](#string-keys-vs-ksec-constants)
20
- - [Centralized Query Builder](#centralized-query-builder)
21
- - [OSStatus Error Handling](#osstatus-error-handling)
22
- - [Actor-Isolated Keychain Manager (iOS 17+ / macOS 14+)](#actor-isolated-keychain-manager-ios-17-macos-14)
23
- - [Why Actors over GCD](#why-actors-over-gcd)
24
- - [Legacy GCD Pattern (iOS 13-16 codebases)](#legacy-gcd-pattern-ios-1316-codebases)
25
- - [Performance Architecture](#performance-architecture)
26
- - [Two-Tier Encryption and Query Cost](#two-tier-encryption-and-query-cost)
27
- - [Query Specificity](#query-specificity)
28
- - [App Launch Performance](#app-launch-performance)
29
- - [Batch Operations](#batch-operations)
30
- - [macOS Keychain Routing (TN3137)](#macos-keychain-routing-tn3137)
31
- - [Accessibility and Data Protection Classes](#accessibility-and-data-protection-classes)
32
- - [Cross-References](#cross-references)
33
- - [Authoritative References](#authoritative-references)
34
- - [Implementation Notes](#implementation-notes)
35
- - [Summary Checklist](#summary-checklist)
36
-
37
- ## Architecture Overview
38
-
39
- The Keychain Services API exposes four C functions that map to database CRUD operations. Every call is an IPC round-trip to the `securityd` daemon, backed by an encrypted SQLite database. This means every call **blocks the calling thread** and must never execute on `@MainActor`.
40
-
41
- Internally, keychain items use **two-tier AES-256-GCM encryption** (per the Apple Platform Security Guide): a table-level **metadata key** cached in the Application Processor for fast attribute searches, and a **per-row secret key** requiring a Secure Enclave round-trip for `kSecValueData` decryption. This two-tier design has direct performance implications covered in the Performance section below.
42
-
43
- ---
44
-
45
- ## The Four Functions and Their Dictionary Contracts
46
-
47
- Each function accepts a specific _type_ of dictionary. Confusing which keys belong in which dictionary is the single most common source of bugs. Quinn (Apple DTS) defines five property groups:
48
-
49
- 1. **Item class** - `kSecClass`
50
- 2. **Item attributes** - `kSecAttrAccount`, `kSecAttrService`, etc.
51
- 3. **Search properties** - `kSecMatchLimit`
52
- 4. **Return type properties** - `kSecReturnData`, `kSecReturnAttributes`, `kSecReturnRef`, `kSecReturnPersistentRef`
53
- 5. **Value type properties** - `kSecValueData`, `kSecValueRef`
54
-
55
- | Function | Dictionary Type | Supports Return Keys? | Default `kSecMatchLimit` | Since |
56
- | --------------------------- | -------------------------------------------- | ----------------------- | ------------------------ | ------- |
57
- | `SecItemAdd(_:_:)` | Add dictionary (class + attrs + values) | ✅ Optional | N/A | iOS 2.0 |
58
- | `SecItemCopyMatching(_:_:)` | Query + return (all 5 groups) | ✅ Required for results | `kSecMatchLimitOne` | iOS 2.0 |
59
- | `SecItemUpdate(_:_:)` | Pure query (param 1) + update dict (param 2) | ❌ | **`kSecMatchLimitAll`** | iOS 2.0 |
60
- | `SecItemDelete(_:)` | Pure query | ❌ | **`kSecMatchLimitAll`** | iOS 2.0 |
61
-
62
- **Critical detail:** `kSecMatchLimit` defaults to `kSecMatchLimitOne` for `SecItemCopyMatching` but **`kSecMatchLimitAll` for `SecItemUpdate` and `SecItemDelete`**. An under-specified delete query will wipe every matching item in the keychain.
63
-
64
- **Dictionary hygiene:** Use a fresh dictionary for each call. Putting `kSecReturnData` in an add dictionary or `kSecClass` in an update dictionary produces `errSecParam` (-50). Quinn's guidance: "Use a new dictionary for each call. That prevents state from one call accidentally leaking into a subsequent call."
65
-
66
- ---
67
-
68
- ## Uniqueness and Primary Keys
69
-
70
- For `kSecClassGenericPassword`, uniqueness is determined by the combination of:
71
-
72
- - `kSecAttrAccount` + `kSecAttrService` + `kSecAttrAccessGroup` + `kSecAttrSynchronizable`
73
-
74
- Other attributes like `kSecAttrGeneric`, `kSecAttrLabel`, or `kSecAttrDescription` **do not participate in uniqueness**. This means a query filtering on non-unique attributes can return `errSecItemNotFound` while a subsequent add still hits `errSecDuplicateItem`.
75
-
76
- For `kSecClassInternetPassword`, the uniqueness set includes: `kSecAttrAccount` + `kSecAttrServer` + `kSecAttrProtocol` + `kSecAttrAuthenticationType` + `kSecAttrPort` + `kSecAttrPath` + `kSecAttrSecurityDomain` + `kSecAttrAccessGroup` + `kSecAttrSynchronizable`.
77
-
78
- **Immutable attributes:** `kSecAttrAccount` and `kSecClass` cannot be changed via `SecItemUpdate`. To change them, delete and re-add the item (see `keychain-item-classes.md`).
79
-
80
- ---
81
-
82
- ## The Add-or-Update Pattern
83
-
84
- The most common AI-generated keychain bug is calling `SecItemAdd` without handling `errSecDuplicateItem` (-25299).
85
-
86
- ❌ **Naive add that silently fails on duplicate:**
14
+ - [How the Keychain Is Built](#how-the-keychain-is-built)
15
+ - [Dictionary Contracts](#dictionary-contracts)
16
+ - [Uniqueness](#uniqueness)
17
+ - [Saving: Add, Then Update on Duplicate](#saving-add-then-update-on-duplicate)
18
+ - [Reading](#reading)
19
+ - [One Place That Builds Queries](#one-place-that-builds-queries)
20
+ - [OSStatus Codes](#osstatus-codes)
21
+ - [An Actor Around the Keychain (iOS 13+ / macOS 10.15+)](#an-actor-around-the-keychain-ios-13--macos-1015)
22
+ - [Performance](#performance)
23
+ - [macOS: Two Keychains (TN3137)](#macos-two-keychains-tn3137)
24
+ - [Accessibility at a Glance](#accessibility-at-a-glance)
25
+ - [Checklist](#checklist)
26
+ - [Related Files](#related-files)
27
+
28
+ ## How the Keychain Is Built
29
+
30
+ The API is four C functions, one per CRUD verb. None of them touches the
31
+ database directly: each call travels over IPC to `securityd`, the daemon
32
+ that owns an encrypted SQLite store. Because the caller waits for that round
33
+ trip, every call blocks its thread. Keep them off `@MainActor`.
34
+
35
+ Items are protected in two tiers, both AES-256-GCM:
36
+
37
+ - the searchable attributes sit under a metadata key that is shared by the
38
+ table and kept cached on the Application Processor;
39
+ - a per-row secret key encrypts `kSecValueData`, and unwrapping it needs a trip
40
+ to the Secure Enclave.
41
+
42
+ That split explains most of the performance advice further down.
43
+
44
+ ## Dictionary Contracts
45
+
46
+ Quinn groups the keys you can put in a SecItem dictionary into five kinds:
47
+
48
+ | Group | Examples |
49
+ | --- | --- |
50
+ | Item class | `kSecClass` |
51
+ | Attributes | `kSecAttrAccount`, `kSecAttrService`, ... |
52
+ | Search | `kSecMatchLimit` |
53
+ | Return type | `kSecReturnData`, `kSecReturnAttributes`, `kSecReturnRef`, `kSecReturnPersistentRef` |
54
+ | Value type | `kSecValueData`, `kSecValueRef` |
55
+
56
+ Each function accepts a different subset (all available since iOS 2.0):
57
+
58
+ | Function | Accepts | Return keys | Default match limit |
59
+ | --- | --- | --- | --- |
60
+ | `SecItemAdd(_:_:)` | class, attributes, values | optional | n/a |
61
+ | `SecItemCopyMatching(_:_:)` | all five groups | needed to get anything back | `kSecMatchLimitOne` |
62
+ | `SecItemUpdate(_:_:)` | a query, plus a second dictionary of changes | not allowed | `kSecMatchLimitAll` |
63
+ | `SecItemDelete(_:)` | a query | not allowed | `kSecMatchLimitAll` |
64
+
65
+ The delete default deserves attention: a query that names only the class, or
66
+ only the service, removes every item it matches.
67
+
68
+ Give every call its own freshly built dictionary instead of reusing one. Leftover keys break the contract: a `kSecReturnData` inside an add
69
+ dictionary, or a `kSecClass` inside an update dictionary, fails with
70
+ `errSecParam` (-50).
71
+
72
+ ## Uniqueness
73
+
74
+ A generic password is identified by the combination of `kSecAttrAccount`,
75
+ `kSecAttrService`, `kSecAttrAccessGroup` and `kSecAttrSynchronizable`.
76
+ `kSecAttrGeneric`, `kSecAttrLabel` and `kSecAttrDescription` play no part in
77
+ that identity.
78
+
79
+ This produces a confusing pair of results: a query that filters on a
80
+ non-identifying attribute such as `kSecAttrGeneric` can report
81
+ `errSecItemNotFound`, while an add with the same service and account still
82
+ reports `errSecDuplicateItem`.
83
+
84
+ Internet passwords are identified by account, server, protocol, authentication
85
+ type, port, path, security domain, access group and synchronizable. See
86
+ [keychain-item-classes.md](keychain-item-classes.md) for every class.
87
+
88
+ `SecItemUpdate` cannot change `kSecClass`: the class belongs to the query,
89
+ and the attributes dictionary accepts only real keychain attributes. To change
90
+ class, delete the old item and add a new one. A primary-key attribute such as
91
+ `kSecAttrAccount` can be updated; the update fails with `errSecDuplicateItem`
92
+ only when another item already has the resulting primary key.
93
+
94
+ ## Saving: Add, Then Update on Duplicate
95
+
96
+ The bug that shows up most in generated keychain code is a `SecItemAdd` whose
97
+ result is never read. The first save works; the second returns
98
+ `errSecDuplicateItem` (-25299), nothing is written, and the app keeps running
99
+ with the stale value.
87
100
 
88
101
  ```swift
89
- // ❌ WRONG - silently fails if item already exists
90
- func savePassword(_ password: String, account: String) {
91
- let query: [CFString: Any] = [
102
+ // Wrong: status ignored, so every save after the first silently does nothing.
103
+ func storeWrong(_ pin: Data) {
104
+ let item: [CFString: Any] = [
92
105
  kSecClass: kSecClassGenericPassword,
93
- kSecAttrService: "com.example.app",
94
- kSecAttrAccount: account,
95
- kSecValueData: Data(password.utf8)
106
+ kSecAttrService: "com.example.lockbox",
107
+ kSecAttrAccount: "vault-pin",
108
+ kSecValueData: pin
96
109
  ]
97
- SecItemAdd(query as CFDictionary, nil) // Return value IGNORED!
98
- // If item exists → errSecDuplicateItem (-25299) - password never saved
110
+ SecItemAdd(item as CFDictionary, nil)
99
111
  }
100
112
  ```
101
113
 
102
- ✅ **Correct add-or-update with exhaustive OSStatus handling:**
103
-
104
114
  ```swift
105
- // ✅ CORRECT - attempts add, falls back to update on duplicate
106
- func savePassword(_ password: String, account: String) throws {
107
- let baseQuery: [CFString: Any] = [
115
+ func store(_ pin: Data) throws {
116
+ let identity: [CFString: Any] = [
108
117
  kSecClass: kSecClassGenericPassword,
109
- kSecAttrService: "com.example.app",
110
- kSecAttrAccount: account
118
+ kSecAttrService: "com.example.lockbox",
119
+ kSecAttrAccount: "vault-pin"
111
120
  ]
121
+ var insert = identity
122
+ insert[kSecValueData] = pin
123
+ insert[kSecAttrAccessible] = kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
112
124
 
113
- var addQuery = baseQuery
114
- addQuery[kSecValueData] = Data(password.utf8)
115
- addQuery[kSecAttrAccessible] = kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
116
-
117
- let addStatus = SecItemAdd(addQuery as CFDictionary, nil)
118
-
119
- switch addStatus {
125
+ let added = SecItemAdd(insert as CFDictionary, nil)
126
+ switch added {
120
127
  case errSecSuccess:
121
128
  return
122
-
123
129
  case errSecDuplicateItem:
124
- // Item exists - update it
125
- let updates: [CFString: Any] = [kSecValueData: Data(password.utf8)]
126
- let updateStatus = SecItemUpdate(
127
- baseQuery as CFDictionary,
128
- updates as CFDictionary
129
- )
130
- guard updateStatus == errSecSuccess else {
131
- throw KeychainError(status: updateStatus)
132
- }
133
-
130
+ let changes: [CFString: Any] = [kSecValueData: pin]
131
+ let updated = SecItemUpdate(identity as CFDictionary, changes as CFDictionary)
132
+ guard updated == errSecSuccess else { throw KeychainError(status: updated) }
134
133
  case errSecInteractionNotAllowed:
135
- // Device locked - do NOT delete-and-retry!
136
- throw KeychainError(status: addStatus)
137
-
134
+ throw KeychainError(status: added)
138
135
  default:
139
- throw KeychainError(status: addStatus)
136
+ throw KeychainError(status: added)
140
137
  }
141
138
  }
142
139
  ```
143
140
 
144
- Key points in this pattern:
145
-
146
- - **Separate dictionaries** for add vs. update - the update dictionary contains only the attributes to change, never `kSecClass` or search properties.
147
- - **`errSecInteractionNotAllowed`** (-25308) means the device is locked and data protection prevents access. Never delete items in response to this error; the item is valid but temporarily inaccessible.
148
- - **Prefer update over delete-then-add** - update preserves persistent references and avoids the race condition window between delete and add.
149
-
150
- ---
141
+ Points to carry into any variant:
151
142
 
152
- ## Reading from the Keychain: Return Flags and Type Casting
143
+ - The changes dictionary lists only what should change. It never contains
144
+ `kSecClass` or the search attributes.
145
+ - `errSecInteractionNotAllowed` (-25308) tells you the item exists but cannot be
146
+ opened now: the device is locked, or the item needs UI that cannot appear. The item is fine. Deleting it here destroys
147
+ good data.
148
+ - Updating in place beats deleting and re-adding: persistent references to the
149
+ item stay valid, and there is no moment where the item is missing for a
150
+ concurrent reader. Delete and re-add only when the access control itself has
151
+ to change (see [keychain-access-control.md](keychain-access-control.md)).
153
152
 
154
- The second most common bug is calling `SecItemCopyMatching` without `kSecReturn*` flags. The function may return `errSecSuccess` with a `nil` result - this is "success but nil," not a real success.
153
+ ## Reading
155
154
 
156
- ❌ **Query that returns no data because `kSecReturnData` is missing:**
155
+ Next in frequency: a `SecItemCopyMatching` that sets no `kSecReturn*` key. The call succeeds and the result is empty.
157
156
 
158
157
  ```swift
159
- // ❌ WRONG - no kSecReturn* flags, result is always nil
160
- func loadPassword(account: String) -> Data? {
161
- let query: [CFString: Any] = [
158
+ // Wrong: no return key, so `out` is nil and the cast always fails.
159
+ func fetchWrong() -> Data? {
160
+ let lookup: [CFString: Any] = [
162
161
  kSecClass: kSecClassGenericPassword,
163
- kSecAttrService: "com.example.app",
164
- kSecAttrAccount: account,
162
+ kSecAttrService: "com.example.lockbox",
163
+ kSecAttrAccount: "vault-pin",
165
164
  kSecMatchLimit: kSecMatchLimitOne
166
- // BUG: Missing kSecReturnData: true
167
165
  ]
168
- var result: CFTypeRef?
169
- SecItemCopyMatching(query as CFDictionary, &result)
170
- return result as? Data // Always nil - no return type was requested
166
+ var out: CFTypeRef?
167
+ SecItemCopyMatching(lookup as CFDictionary, &out)
168
+ return out as? Data
171
169
  }
172
170
  ```
173
171
 
174
- ✅ **Correct query with proper return flags and exhaustive error handling:**
175
-
176
172
  ```swift
177
- // ✅ CORRECT - explicitly requests data, handles all error states
178
- func loadPassword(account: String) throws -> Data? {
179
- let query: [CFString: Any] = [
173
+ func fetch() throws -> Data? {
174
+ let lookup: [CFString: Any] = [
180
175
  kSecClass: kSecClassGenericPassword,
181
- kSecAttrService: "com.example.app",
182
- kSecAttrAccount: account,
176
+ kSecAttrService: "com.example.lockbox",
177
+ kSecAttrAccount: "vault-pin",
183
178
  kSecMatchLimit: kSecMatchLimitOne,
184
- kSecReturnData: true // ← REQUIRED to get the secret
179
+ kSecReturnData: true
185
180
  ]
186
-
187
- var result: CFTypeRef?
188
- let status = SecItemCopyMatching(query as CFDictionary, &result)
189
-
181
+ var out: CFTypeRef?
182
+ let status = SecItemCopyMatching(lookup as CFDictionary, &out)
190
183
  switch status {
191
184
  case errSecSuccess:
192
- guard let data = result as? Data else {
193
- throw KeychainError(status: errSecParam)
194
- }
195
- return data
196
-
185
+ guard let bytes = out as? Data else { throw KeychainError(status: errSecParam) }
186
+ return bytes
197
187
  case errSecItemNotFound:
198
- return nil // Legitimate "not found" - not an error
199
-
188
+ return nil
200
189
  case errSecInteractionNotAllowed:
201
190
  throw KeychainError(status: status)
202
-
203
191
  default:
204
192
  throw KeychainError(status: status)
205
193
  }
206
194
  }
207
195
  ```
208
196
 
209
- ### Return Type Cheat Sheet
210
-
211
- The `CFTypeRef` type depends entirely on which return flags and match limits are set:
212
-
213
- ```text
214
- kSecReturnData only + kSecMatchLimitOne → Data
215
- kSecReturnAttributes + kSecMatchLimitOne → [String: Any]
216
- kSecReturnData + Attrs + kSecMatchLimitOne → [String: Any] (data under kSecValueData key)
217
- kSecReturnRef + kSecMatchLimitOne → SecKey / SecCertificate / SecIdentity
218
- kSecReturnPersistentRef + kSecMatchLimitOne → Data (opaque handle)
219
- Any combination + kSecMatchLimitAll → Array of the above type
220
- ```
221
-
222
- **Note:** Combining `kSecReturnData` with `kSecMatchLimitAll` may be restricted for password classes on some OS versions. For listing items, prefer `kSecReturnAttributes` or `kSecReturnRef` with `kSecMatchLimitAll`, then fetch data per-item as needed.
223
-
224
- ### String Keys vs. kSec\* Constants
225
-
226
- Never use raw string literals (`"svce"`, `"class"`) instead of `kSec*` constants. The constants are `CFString` values with specific internal representations. Two equally valid dictionary key styles exist:
227
-
228
- ```swift
229
- // Style A: CFString keys (fewer casts at definition, cast once at call site)
230
- let query: [CFString: Any] = [kSecClass: kSecClassGenericPassword]
231
- SecItemAdd(query as CFDictionary, nil)
197
+ What lands in `out` depends on the return keys and the match limit:
232
198
 
233
- // Style B: String keys (more common in community code)
234
- let query: [String: Any] = [kSecClass as String: kSecClassGenericPassword]
235
- SecItemAdd(query as CFDictionary, nil)
236
- ```
199
+ | Return keys | `kSecMatchLimitOne` gives | `kSecMatchLimitAll` gives |
200
+ | --- | --- | --- |
201
+ | Data | `Data` | array of `Data` |
202
+ | Attributes | `[String: Any]` | array of dictionaries |
203
+ | Data + Attributes | dictionary, secret under `kSecValueData` | array of dictionaries |
204
+ | Ref | `SecKey`, `SecCertificate` or `SecIdentity` | array of refs |
205
+ | PersistentRef | opaque `Data` | array of `Data` |
237
206
 
238
- Both are correct. Pick one style and use it consistently across your codebase.
207
+ Some OS versions refuse `kSecReturnData` combined with `kSecMatchLimitAll` for
208
+ password classes. List items with attributes or refs, then fetch data for the
209
+ one you need.
239
210
 
240
- ---
211
+ Use the `kSec*` constants, never their string values such as `"svce"` or
212
+ `"class"`. Dictionaries typed `[CFString: Any]` work, and so do `[String: Any]` ones
213
+ whose keys are cast with `as String`; pick one style per codebase. This file uses `[CFString: Any]` because it is
214
+ shorter.
241
215
 
242
- ## Centralized Query Builder
216
+ ## One Place That Builds Queries
243
217
 
244
- Centralize query construction to prevent flag omissions and key typos:
218
+ A small builder keeps every query shape in one auditable spot, so a missing
219
+ return flag or a typo in an attribute shows up in one review instead of twenty.
245
220
 
246
221
  ```swift
247
- enum KeychainQueryBuilder {
248
- static func buildQuery(
249
- forClass secClass: CFString = kSecClassGenericPassword,
250
- account: String? = nil,
251
- service: String? = nil,
222
+ enum KeychainQuery {
223
+ static func make(
224
+ itemClass: CFString = kSecClassGenericPassword,
225
+ account: String,
226
+ service: String,
252
227
  accessGroup: String? = nil,
253
- returnData: Bool = false,
254
- returnAttributes: Bool = false,
255
- matchLimit: CFString = kSecMatchLimitOne
256
- ) -> [String: Any] {
257
- var query: [String: Any] = [kSecClass as String: secClass]
258
-
259
- if let account { query[kSecAttrAccount as String] = account }
260
- if let service { query[kSecAttrService as String] = service }
261
- if let group = accessGroup { query[kSecAttrAccessGroup as String] = group }
262
- if returnData { query[kSecReturnData as String] = kCFBooleanTrue! }
263
- if returnAttributes { query[kSecReturnAttributes as String] = kCFBooleanTrue! }
264
- query[kSecMatchLimit as String] = matchLimit
265
-
266
- return query
228
+ wantData: Bool = false,
229
+ wantAttributes: Bool = false,
230
+ limit: CFString = kSecMatchLimitOne
231
+ ) -> [CFString: Any] {
232
+ var q: [CFString: Any] = [
233
+ kSecClass: itemClass,
234
+ kSecAttrAccount: account,
235
+ kSecAttrService: service,
236
+ kSecMatchLimit: limit
237
+ ]
238
+ if let accessGroup { q[kSecAttrAccessGroup] = accessGroup }
239
+ if wantData { q[kSecReturnData] = true }
240
+ if wantAttributes { q[kSecReturnAttributes] = true }
241
+ return q
267
242
  }
268
243
  }
269
244
  ```
270
245
 
271
- This pattern ensures return flags are set deliberately and provides a single site to audit query construction.
272
-
273
- ---
246
+ Drop `kSecMatchLimit` and the return flags before handing a builder result to
247
+ `SecItemAdd`, `SecItemUpdate` or `SecItemDelete`.
274
248
 
275
- ## OSStatus Error Handling
249
+ ## OSStatus Codes
276
250
 
277
- Never treat all non-zero `OSStatus` values as fatal errors. Several codes represent expected operational states:
251
+ A nonzero status is not automatically a failure to report. Treat each one on
252
+ its merits:
278
253
 
279
- | OSStatus Code | Constant | Meaning | Correct Response |
280
- | ------------- | ----------------------------- | -------------------------------------------------- | ------------------------------------------ |
281
- | `0` | `errSecSuccess` | Operation succeeded | Proceed normally |
282
- | `-25299` | `errSecDuplicateItem` | Item already exists (on add) | Fall back to `SecItemUpdate` |
283
- | `-25300` | `errSecItemNotFound` | No matching item found | Return `nil` / treat as success for delete |
284
- | `-25308` | `errSecInteractionNotAllowed` | Device locked, data protection active | Retry later - **never delete** |
285
- | `-25293` | `errSecUserCanceled` | User cancelled biometric prompt | Propagate cancellation to UI |
286
- | `-50` | `errSecParam` | Invalid parameter / wrong dictionary keys | Developer error - fix query |
287
- | `-25244` | `errSecNoSuchAttr` | Attribute not supported (data protection keychain) | Check for unsupported attributes |
288
-
289
- Map raw codes to a domain-specific Swift error:
254
+ | Code | Constant | Response |
255
+ | --- | --- | --- |
256
+ | 0 | `errSecSuccess` | Continue. |
257
+ | -25299 | `errSecDuplicateItem` | Switch to `SecItemUpdate`. |
258
+ | -25300 | `errSecItemNotFound` | Return nil; on delete, count it as success. |
259
+ | -25308 | `errSecInteractionNotAllowed` | Try again later (device locked). Never delete. |
260
+ | -128 | `errSecUserCanceled` | The user dismissed the prompt; tell the UI. |
261
+ | -25293 | `errSecAuthFailed` | Authentication failed or the protected item is no longer usable. |
262
+ | -50 | `errSecParam` | A programming error in the dictionary; fix the query. |
263
+ | -25303 | `errSecNoSuchAttr` | The attribute is not supported (seen on the data protection keychain). |
290
264
 
291
265
  ```swift
292
266
  struct KeychainError: Error, CustomStringConvertible {
293
267
  let status: OSStatus
294
-
295
268
  var description: String {
296
- let msg = SecCopyErrorMessageString(status, nil) as String? ?? "Unknown"
297
- return "KeychainError(\(status)): \(msg)"
269
+ let text = SecCopyErrorMessageString(status, nil) as String? ?? "unknown"
270
+ return "Keychain status \(status): \(text)"
298
271
  }
299
272
  }
300
273
  ```
301
274
 
302
- **Logging safety:** Log only the query shape and resulting status code. Never log secret data (`kSecValueData`), tokens, or keys.
303
-
304
- ---
305
-
306
- ## Actor-Isolated Keychain Manager (iOS 17+ / macOS 14+)
275
+ Logs may record the shape of a query (class, service, which flags were set)
276
+ and the status code. They never record `kSecValueData`, tokens or key bytes.
307
277
 
308
- Every `SecItem*` function blocks the calling thread due to IPC to `securityd` and potential Secure Enclave round-trips. For biometry-protected items, the block can last several seconds during user authentication (WWDC 2014 Session 711).
278
+ ## An Actor Around the Keychain (iOS 13+ / macOS 10.15+)
309
279
 
310
- ❌ **`@MainActor` keychain access that blocks the UI:**
280
+ Reads of biometric-protected items wait for the user and can take several
281
+ seconds (WWDC 2014 Session 711). Doing that on the main actor freezes the UI.
311
282
 
312
283
  ```swift
313
- // ❌ WRONG - blocks main thread, freezes UI during securityd IPC
284
+ // Wrong: the main actor waits on securityd and, for protected items, on the user.
314
285
  @MainActor
315
- class SettingsViewModel: ObservableObject {
316
- @Published var token: String = ""
317
-
318
- func loadToken() {
319
- let query: [CFString: Any] = [
320
- kSecClass: kSecClassGenericPassword,
321
- kSecAttrService: "com.example.app",
322
- kSecAttrAccount: "authToken",
323
- kSecReturnData: true,
324
- kSecMatchLimit: kSecMatchLimitOne
325
- ]
326
- var result: CFTypeRef?
327
- let status = SecItemCopyMatching(query as CFDictionary, &result)
328
- if status == errSecSuccess, let data = result as? Data {
329
- self.token = String(data: data, encoding: .utf8) ?? ""
330
- }
331
- // UI frozen for entire duration of securityd IPC + potential SE round-trip
286
+ final class ProfileModel: ObservableObject {
287
+ @Published var apiKey: String?
288
+ func load() {
289
+ var out: CFTypeRef?
290
+ let q: [CFString: Any] = [kSecClass: kSecClassGenericPassword,
291
+ kSecAttrAccount: "api", kSecReturnData: true]
292
+ _ = SecItemCopyMatching(q as CFDictionary, &out)
293
+ apiKey = (out as? Data).flatMap { String(decoding: $0, as: UTF8.self) }
332
294
  }
333
295
  }
334
296
  ```
335
297
 
336
- ✅ **Actor-isolated keychain manager with full CRUD:**
337
-
338
298
  ```swift
339
- // ✅ CORRECT - dedicated actor keeps all SecItem calls off @MainActor
340
- actor KeychainManager {
341
- static let shared = KeychainManager()
342
-
299
+ actor SecretStore {
300
+ static let shared = SecretStore()
343
301
  private let service: String
344
302
 
345
- init(service: String = Bundle.main.bundleIdentifier ?? "default") {
303
+ init(service: String = Bundle.main.bundleIdentifier ?? "app.secrets") {
346
304
  self.service = service
347
305
  }
348
306
 
349
- // MARK: - Save (add-or-update)
350
-
351
- func save(_ data: Data, for key: String,
352
- accessibility: CFTypeRef = kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
353
- ) throws {
354
- let baseQuery: [CFString: Any] = [
355
- kSecClass: kSecClassGenericPassword,
356
- kSecAttrService: service,
357
- kSecAttrAccount: key
358
- ]
359
-
360
- var addQuery = baseQuery
361
- addQuery[kSecValueData] = data
362
- addQuery[kSecAttrAccessible] = accessibility
363
-
364
- let addStatus = SecItemAdd(addQuery as CFDictionary, nil)
365
-
366
- switch addStatus {
367
- case errSecSuccess:
368
- return
369
- case errSecDuplicateItem:
370
- let updates: [CFString: Any] = [kSecValueData: data]
371
- let updateStatus = SecItemUpdate(
372
- baseQuery as CFDictionary,
373
- updates as CFDictionary
374
- )
375
- guard updateStatus == errSecSuccess else {
376
- throw KeychainError(status: updateStatus)
377
- }
378
- case errSecInteractionNotAllowed:
379
- throw KeychainError(status: addStatus)
380
- default:
381
- throw KeychainError(status: addStatus)
382
- }
307
+ private func identity(_ account: String) -> [CFString: Any] {
308
+ [kSecClass: kSecClassGenericPassword,
309
+ kSecAttrService: service,
310
+ kSecAttrAccount: account]
383
311
  }
384
312
 
385
- // MARK: - Load
386
-
387
- func load(for key: String) throws -> Data? {
388
- let query: [CFString: Any] = [
389
- kSecClass: kSecClassGenericPassword,
390
- kSecAttrService: service,
391
- kSecAttrAccount: key,
392
- kSecReturnData: true,
393
- kSecMatchLimit: kSecMatchLimitOne
394
- ]
395
-
396
- var result: CFTypeRef?
397
- let status = SecItemCopyMatching(query as CFDictionary, &result)
398
-
399
- switch status {
400
- case errSecSuccess:
401
- return result as? Data
402
- case errSecItemNotFound:
403
- return nil
404
- case errSecInteractionNotAllowed:
405
- throw KeychainError(status: status)
406
- default:
407
- throw KeychainError(status: status)
408
- }
313
+ func put(_ value: Data, for account: String,
314
+ accessible: CFString = kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly) throws {
315
+ var insert = identity(account)
316
+ insert[kSecValueData] = value
317
+ insert[kSecAttrAccessible] = accessible
318
+ let status = SecItemAdd(insert as CFDictionary, nil)
319
+ if status == errSecSuccess { return }
320
+ guard status == errSecDuplicateItem else { throw KeychainError(status: status) }
321
+ let changed = SecItemUpdate(identity(account) as CFDictionary,
322
+ [kSecValueData: value] as CFDictionary)
323
+ guard changed == errSecSuccess else { throw KeychainError(status: changed) }
409
324
  }
410
325
 
411
- // MARK: - Delete (idempotent)
326
+ func get(_ account: String) throws -> Data? {
327
+ var q = identity(account)
328
+ q[kSecReturnData] = true
329
+ q[kSecMatchLimit] = kSecMatchLimitOne
330
+ var out: CFTypeRef?
331
+ let status = SecItemCopyMatching(q as CFDictionary, &out)
332
+ if status == errSecItemNotFound { return nil }
333
+ guard status == errSecSuccess else { throw KeychainError(status: status) }
334
+ return out as? Data
335
+ }
412
336
 
413
- func delete(key: String) throws {
414
- let query: [CFString: Any] = [
415
- kSecClass: kSecClassGenericPassword,
416
- kSecAttrService: service,
417
- kSecAttrAccount: key
418
- ]
419
- let status = SecItemDelete(query as CFDictionary)
337
+ func remove(_ account: String) throws {
338
+ let status = SecItemDelete(identity(account) as CFDictionary)
420
339
  guard status == errSecSuccess || status == errSecItemNotFound else {
421
340
  throw KeychainError(status: status)
422
341
  }
423
342
  }
424
343
 
425
- // MARK: - List all accounts (attributes only - fast)
426
-
427
- func allAccounts() throws -> [String] {
428
- let query: [CFString: Any] = [
429
- kSecClass: kSecClassGenericPassword,
430
- kSecAttrService: service,
431
- kSecMatchLimit: kSecMatchLimitAll,
432
- kSecReturnAttributes: true // No kSecReturnData → skips SE round-trip
433
- ]
434
-
435
- var result: CFTypeRef?
436
- let status = SecItemCopyMatching(query as CFDictionary, &result)
437
-
438
- switch status {
439
- case errSecSuccess:
440
- guard let items = result as? [[String: Any]] else { return [] }
441
- return items.compactMap { $0[kSecAttrAccount as String] as? String }
442
- case errSecItemNotFound:
443
- return []
444
- default:
344
+ func accounts() throws -> [String] {
345
+ let q: [CFString: Any] = [kSecClass: kSecClassGenericPassword,
346
+ kSecAttrService: service,
347
+ kSecMatchLimit: kSecMatchLimitAll,
348
+ kSecReturnAttributes: true]
349
+ var out: CFTypeRef?
350
+ let status = SecItemCopyMatching(q as CFDictionary, &out)
351
+ if status == errSecItemNotFound { return [] }
352
+ guard status == errSecSuccess, let rows = out as? [[String: Any]] else {
445
353
  throw KeychainError(status: status)
446
354
  }
355
+ return rows.compactMap { $0[kSecAttrAccount as String] as? String }
447
356
  }
448
357
  }
449
358
  ```
450
359
 
451
- **Calling from SwiftUI:**
360
+ `remove` is idempotent. `accounts()` asks for attributes only, so it never pays
361
+ for the Secure Enclave unwrap of each secret.
362
+
363
+ From SwiftUI, the view model awaits the actor and the main actor is free while
364
+ the keychain works:
452
365
 
453
366
  ```swift
454
367
  @MainActor
455
- class AuthViewModel: ObservableObject {
456
- @Published var isAuthenticated = false
457
-
458
- func loadToken() async {
459
- do {
460
- // Crosses actor boundary - suspends, does NOT block MainActor
461
- let data = try await KeychainManager.shared.load(for: "authToken")
462
- isAuthenticated = data != nil
463
- } catch {
464
- isAuthenticated = false
465
- }
368
+ @Observable
369
+ final class SettingsModel {
370
+ var hasKey = false
371
+ func refresh() async {
372
+ hasKey = (try? await SecretStore.shared.get("api")) != nil
466
373
  }
467
374
  }
468
375
  ```
469
376
 
470
- ### Why Actors over GCD
377
+ Why an actor rather than a queue:
471
378
 
472
- | Dimension | Actor (iOS 17+) | GCD Serial Queue |
473
- | --------------------- | --------------------------------- | --------------------------------------- |
474
- | UI blocking | Low - compiler-enforced isolation | Low (if dispatched correctly) |
475
- | Thread safety | Serialized by actor runtime | Manual - developer discipline |
476
- | Readability | Linear async/await | Nested completion handlers |
477
- | Compiler guarantees | Enforced `Sendable` + isolation | None - silent data races possible |
478
- | Swift 6 compatibility | Native - actors are `Sendable` | Requires manual `@Sendable` annotations |
379
+ | | Actor | GCD serial queue |
380
+ | --- | --- | --- |
381
+ | Isolation | Checked by the compiler | Up to the developer |
382
+ | Serialization | Done by the runtime | Done by the queue, if used consistently |
383
+ | Control flow | Straight-line `async`/`await` | Nested completion handlers |
384
+ | `Sendable` | Enforced | Annotate `@Sendable` by hand |
385
+ | Swift 6 | Native | Needs care to pass strict checking |
479
386
 
480
- ### Legacy GCD Pattern (iOS 13-16 codebases)
387
+ In completion-handler code that has not moved to Swift concurrency, a private
388
+ serial queue does the same job:
481
389
 
482
390
  ```swift
483
- class LegacyKeychainManager {
484
- private let queue = DispatchQueue(label: "com.app.keychain",
485
- qos: .userInitiated)
486
-
487
- func load(key: String, completion: @escaping (Result<Data?, Error>) -> Void) {
391
+ final class LegacySecretStore {
392
+ private let queue = DispatchQueue(label: "secrets.io", qos: .userInitiated)
393
+ func get(_ account: String, completion: @escaping @Sendable (Result<Data?, Error>) -> Void) {
488
394
  queue.async {
489
- // ... SecItemCopyMatching on background queue ...
395
+ let result = Result { try readItem(account: account) }
490
396
  DispatchQueue.main.async { completion(result) }
491
397
  }
492
398
  }
493
399
  }
494
400
  ```
495
401
 
496
- ---
497
-
498
- ## Performance Architecture
499
-
500
- ### Two-Tier Encryption and Query Cost
501
-
502
- Because of the two-tier encryption design:
503
-
504
- - **`kSecReturnAttributes` only** → uses cached metadata key → **fast** (no Secure Enclave round-trip)
505
- - **`kSecReturnData`** → requires per-row secret key from Secure Enclave → **slower**
506
-
507
- For listing operations, always use `kSecReturnAttributes` or `kSecReturnRef` and fetch secret data only for the specific item the user selects.
508
-
509
- ### Query Specificity
510
-
511
- The underlying SQLite database benefits from narrow constraints. A query specifying only `kSecClass: kSecClassGenericPassword` with `kSecMatchLimitAll` performs a **full table scan**. Adding `kSecAttrService` and `kSecAttrAccount` enables indexed lookup. Always include all relevant uniqueness attributes in production queries.
512
-
513
- ### App Launch Performance
514
-
515
- Keychain access during app launch is a measurable performance risk:
516
-
517
- - Each call requires IPC to `securityd` plus potential Secure Enclave latency
518
- - Items with `kSecAttrAccessibleWhenUnlocked` may be unavailable before first unlock (iOS can launch apps before the user unlocks - e.g., background refresh, VoIP pushes)
519
- - **Best practice:** Defer keychain reads until actually needed. Never call SecItem synchronously in `application(_:didFinishLaunchingWithOptions:)`.
520
- - Handle `errSecInteractionNotAllowed` gracefully - never destructively.
521
-
522
- ### Batch Operations
523
-
524
- There is **no batch API** for SecItem. Each function operates individually with one partial exception: `SecItemAdd` supports `kSecUseItemList` to add multiple certificates or keys (not passwords) in a single call. For batch reads, `SecItemCopyMatching` with `kSecMatchLimitAll` retrieves all matching items at once.
525
-
526
- ---
527
-
528
- ## macOS Keychain Routing (TN3137)
529
-
530
- On macOS, the SecItem API can target two different implementations:
531
-
532
- | Implementation | Activated By | Behavior |
533
- | ------------------------------ | -------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
534
- | **Legacy file-based keychain** | Default on macOS (without opt-in) | Silently ignores unsupported attributes; inconsistent `kSecMatchLimit` defaults; different `SecItemAdd` return types |
535
- | **Data protection keychain** | `kSecUseDataProtectionKeychain: true` (macOS 10.15+) or `kSecAttrSynchronizable: true` | Parity with iOS; required for iCloud Keychain sync, biometric protection, and Secure Enclave key storage |
536
-
537
- **Modern apps must always target the data protection keychain.** Mac Catalyst and iOS Apps on Mac use it automatically.
402
+ Here `readItem(account:)` stands for a synchronous read such as `fetch()` in
403
+ the Reading section.
404
+ The completion is `@Sendable` because it crosses two queues; without it Swift 6
405
+ reports sending `completion` as a data race.
406
+
407
+ ## Performance
408
+
409
+ - Returning attributes only uses the cached metadata key. It is quick and needs
410
+ no Secure Enclave work.
411
+ - Returning data needs the per-row key from the Secure Enclave, so it costs
412
+ more.
413
+ - For a list screen, fetch attributes or refs, and fetch data only for the row
414
+ the user picks.
415
+ - A query with nothing but `kSecClass` and `kSecMatchLimitAll` scans the whole
416
+ table. Adding service and account lets `securityd` use its index.
417
+ - At launch every read costs IPC plus enclave latency, and items protected with
418
+ a `WhenUnlocked` class can be unavailable if the app was launched before first
419
+ unlock (background refresh, VoIP pushes).
420
+ - Do not call SecItem synchronously in
421
+ `application(_:didFinishLaunchingWithOptions:)`. Read the item when a feature
422
+ first needs it.
423
+ - Passwords cannot be written in bulk. `SecItemAdd` with `kSecUseItemList` can
424
+ add several certificates or keys at once, but not password items. Reading in
425
+ bulk means `kSecMatchLimitAll`.
426
+
427
+ ## macOS: Two Keychains (TN3137)
428
+
429
+ A Mac has two keychain implementations:
430
+
431
+ - the legacy **file-based keychain**, which SecItem uses unless told otherwise;
432
+ - the **data protection keychain**, which matches iOS.
433
+
434
+ The file-based one ignores attributes it does not understand without saying
435
+ so, applies `kSecMatchLimit` defaults inconsistently, and returns different
436
+ types from `SecItemAdd`.
437
+
438
+ Code opts into the data protection keychain with
439
+ `kSecUseDataProtectionKeychain: true`, available from macOS 10.15, or by setting
440
+ `kSecAttrSynchronizable: true`. It is required for iCloud Keychain sync,
441
+ biometric protection and storing Secure Enclave keys. New code should always
442
+ target it. Mac Catalyst apps and iOS apps running on a Mac use it
443
+ automatically.
538
444
 
539
445
  ```swift
540
- // macOS: Always opt into data protection keychain
541
- var query: [CFString: Any] = [
542
- kSecClass: kSecClassGenericPassword,
543
- kSecAttrService: "com.example.app",
544
- kSecAttrAccount: "token"
545
- ]
546
- #if os(macOS)
547
- query[kSecUseDataProtectionKeychain] = true
548
- #endif
446
+ func macSafe(_ query: [CFString: Any]) -> [CFString: Any] {
447
+ var q = query
448
+ #if os(macOS)
449
+ q[kSecUseDataProtectionKeychain] = true
450
+ #endif
451
+ return q
452
+ }
549
453
  ```
550
454
 
551
- The file-based keychain's shim layer has documented bugs - it silently ignores unsupported attributes where the data protection keychain correctly returns `errSecNoSuchAttr` (-25244). Debugging keychain issues on macOS often starts with confirming which implementation is in use.
552
-
553
- ---
554
-
555
- ## Accessibility and Data Protection Classes
556
-
557
- The `kSecAttrAccessible` attribute controls when a keychain item's secret data can be decrypted. Brief guidance here; see `keychain-access-control.md` for full coverage.
558
-
559
- | Constant | Available When | Survives Backup? | Use Case |
560
- | -------------------------------------------------- | -------------------------------- | ---------------- | -------------------------------------------- |
561
- | `kSecAttrAccessibleWhenUnlockedThisDeviceOnly` | After unlock, until lock | No (device-only) | Default for most secrets |
562
- | `kSecAttrAccessibleAfterFirstUnlock` | After first unlock until restart | Yes | Background processing tokens |
563
- | `kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly` | Only if passcode set + unlocked | No | Highest-sensitivity data (OWASP recommended) |
564
- | `kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly` | After first unlock until restart | No | Background + device-only |
565
-
566
- **Deprecated:** `kSecAttrAccessibleAlways` - deprecated in iOS 12, unsupported on Apple Silicon Macs. Never use.
567
-
568
- ---
569
-
570
- ## Cross-References
571
-
572
- - **Item class deep dive** (required vs optional attributes per kSecClass) → `keychain-item-classes.md`
573
- - **Access control flags and SecAccessControl** → `keychain-access-control.md`
574
- - **Biometric-gated keychain access** (LAContext integration) → `biometric-authentication.md`
575
- - **Secure Enclave key storage** → `secure-enclave.md`
576
- - **Credential lifecycle patterns** (OAuth tokens, API keys) → `credential-storage-patterns.md`
577
- - **Access groups and sharing** → `keychain-sharing.md`
578
- - **Testing keychain code** (mocks, CI/CD) → `testing-security-code.md`
579
- - **Common anti-patterns** (comprehensive catalog) → `common-anti-patterns.md`
580
-
581
- ---
582
-
583
- ## Authoritative References
584
-
585
- | Source | Relevance |
586
- | ------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------- |
587
- | [Keychain Services](https://sosumi.ai/documentation/security/keychain_services) | Main API landing page |
588
- | [TN3137: On Mac Keychain APIs and Implementations](https://sosumi.ai/documentation/technotes/tn3137-on-mac-keychains) | macOS data protection vs file-based routing |
589
- | Quinn "The Eskimo!" - "SecItem: Fundamentals" / "SecItem: Pitfalls and Best Practices" | Most practical DTS reference, updated through 2025 |
590
- | [Apple Platform Security Guide](https://support.apple.com/guide/security/welcome/web) - Keychain Data Protection chapter | Two-tier encryption architecture |
591
- | WWDC 2014 Session 711 - "Keychain and Authentication with Touch ID" | Touch ID/keychain integration patterns |
592
- | WWDC 2019 Session 516 - "What's New in Authentication" | Modern credential management |
593
-
594
- ---
595
-
596
- ## Implementation Notes
597
-
598
- 1. **Dictionary key type convention:** Both `[CFString: Any]` and `[String: Any]` with `kSec* as String` casts are valid. This file uses `[CFString: Any]` for concise examples and shows both styles in the String Keys section.
599
-
600
- 2. **Default accessibility recommendation:** `kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly` is strongest for high-security secrets, but items become unavailable if the user removes their passcode. `kSecAttrAccessibleWhenUnlockedThisDeviceOnly` is a safe general foreground-only default. The actor manager example uses `AfterFirstUnlockThisDeviceOnly` for background compatibility while remaining device-bound.
601
-
602
- 3. **`kSecReturnData` + `kSecMatchLimitAll`:** Some OS versions and keychain implementations have restrictions around returning data for multiple password-class matches. The safer pattern is to use `kSecReturnRef` or `kSecReturnAttributes` with `LimitAll`, then fetch data per item. See the Return Type Cheat Sheet.
603
-
604
- ---
605
-
606
- ## Summary Checklist
607
-
608
- Before shipping keychain code, verify:
609
-
610
- 1. **OSStatus checked on every call** - exhaustive `switch` covering at minimum `errSecSuccess`, `errSecDuplicateItem`, `errSecItemNotFound`, `errSecInteractionNotAllowed`; no ignored return values
611
- 2. **Add-or-update pattern implemented** - `SecItemAdd` catches `-25299` and falls back to `SecItemUpdate`; duplicate saves never crash or silently fail
612
- 3. **Return flags explicitly set** - every `SecItemCopyMatching` call includes at least one `kSecReturn*` flag; no "success but nil" bugs
613
- 4. **CFTypeRef cast matches flags** - cast type corresponds to the combination of return flags and match limit (see Return Type Cheat Sheet)
614
- 5. **Zero SecItem calls on `@MainActor`** - all keychain access isolated in a dedicated `actor` (iOS 17+) or serial `DispatchQueue` (iOS 13-16)
615
- 6. **Fresh dictionaries per call** - no dictionary reuse across SecItem functions; add dict, query dict, and update dict are separate
616
- 7. **kSec\* constants used** - no raw string literals for dictionary keys; using either `[CFString: Any]` or `[String: Any]` with `as String` casts
617
- 8. **Queries are specific** - `kSecAttrService` + `kSecAttrAccount` included for GenericPassword; `kSecMatchLimitOne` used unless enumeration is needed
618
- 9. **Delete treats not-found as success** - `errSecItemNotFound` on delete is a valid postcondition, not an error
619
- 10. **macOS targets data protection keychain** - `kSecUseDataProtectionKeychain: true` set for macOS targets (automatic for Catalyst/iOS-on-Mac)
620
- 11. **errSecInteractionNotAllowed handled non-destructively** - device-locked state triggers retry-later logic, never delete-and-recreate
455
+ When a macOS keychain bug appears, first confirm which backend the call went
456
+ to. An attribute the file-based keychain quietly drops will produce
457
+ `errSecNoSuchAttr` (-25303) on the data protection keychain.
458
+
459
+ ## Accessibility at a Glance
460
+
461
+ | Constant | Readable | In backups | Typical use |
462
+ | --- | --- | --- | --- |
463
+ | `kSecAttrAccessibleWhenUnlockedThisDeviceOnly` | while unlocked | no | the default for most secrets |
464
+ | `kSecAttrAccessibleAfterFirstUnlock` | from first unlock until restart | yes | tokens read in the background |
465
+ | `kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly` | unlocked and a passcode is set | no | the most sensitive data (OWASP recommendation) |
466
+ | `kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly` | after the first unlock, until reboot | no | device-bound items read in the background |
467
+
468
+ `kSecAttrAccessibleAlways` was deprecated in iOS 12 and is not supported on
469
+ Apple silicon Macs. Do not use it. The full model is in
470
+ [keychain-access-control.md](keychain-access-control.md).
471
+
472
+ Choosing among them: `WhenPasscodeSetThisDeviceOnly` is the strongest, but its
473
+ items are deleted if the user removes the passcode. `WhenUnlockedThisDeviceOnly`
474
+ is a safe default for foreground use. The actor above defaults to
475
+ `AfterFirstUnlockThisDeviceOnly` because it assumes background callers.
476
+
477
+ ## Checklist
478
+
479
+ 1. Every SecItem call switches over its status, covering success, duplicate,
480
+ not found and interaction-not-allowed.
481
+ 2. Saves fall back to `SecItemUpdate` on -25299.
482
+ 3. Every `SecItemCopyMatching` sets at least one `kSecReturn*` key.
483
+ 4. The cast of the `CFTypeRef` result matches the return keys and match limit.
484
+ 5. No SecItem call runs on `@MainActor` (an actor, available from iOS 13, or
485
+ a private serial queue in completion-handler code).
486
+ 6. Add, query and update dictionaries are built fresh for each call.
487
+ 7. Keys are `kSec*` constants, not string literals.
488
+ 8. Queries are specific: service and account for generic passwords,
489
+ `kSecMatchLimitOne` unless listing.
490
+ 9. Delete treats `errSecItemNotFound` as success.
491
+ 10. macOS code sets `kSecUseDataProtectionKeychain: true`.
492
+ 11. `errSecInteractionNotAllowed` never leads to deletion.
493
+
494
+ ## Related Files
495
+
496
+ - [keychain-item-classes.md](keychain-item-classes.md), [keychain-access-control.md](keychain-access-control.md),
497
+ [biometric-authentication.md](biometric-authentication.md), [secure-enclave.md](secure-enclave.md)
498
+ - [credential-storage-patterns.md](credential-storage-patterns.md), [keychain-sharing.md](keychain-sharing.md),
499
+ [testing-security-code.md](testing-security-code.md), [common-anti-patterns.md](common-anti-patterns.md)
500
+
501
+ Further reading: the Keychain Services documentation; TN3137; Quinn's two SecItem
502
+ forum posts; the keychain data protection chapter of the Apple Platform
503
+ Security Guide; WWDC 2014 session 711, titled Keychain and Authentication
504
+ with Touch ID; WWDC 2019 session 516, titled What's New in Authentication.