@mmerterden/multi-agent-pipeline 20.7.0 → 20.8.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.
Files changed (264) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/LICENSE +0 -10
  3. package/docs/facts.json +1 -1
  4. package/manifest.json +266 -267
  5. package/package.json +2 -2
  6. package/pipeline/scripts/_notices.mjs +1 -1
  7. package/pipeline/skills/.skill-manifest.json +68 -68
  8. package/pipeline/skills/shared/README.md +70 -70
  9. package/pipeline/skills/shared/external/alarmkit/SKILL.md +373 -381
  10. package/pipeline/skills/shared/external/alarmkit/evals/evals.json +23 -18
  11. package/pipeline/skills/shared/external/alarmkit/references/alarmkit-patterns.md +328 -378
  12. package/pipeline/skills/shared/external/app-clips/SKILL.md +260 -160
  13. package/pipeline/skills/shared/external/app-clips/evals/evals.json +27 -27
  14. package/pipeline/skills/shared/external/app-clips/references/data-handoff-notifications-location.md +150 -83
  15. package/pipeline/skills/shared/external/app-clips/references/routing-and-experiences.md +135 -83
  16. package/pipeline/skills/shared/external/app-clips/references/size-capabilities-and-promotion.md +143 -85
  17. package/pipeline/skills/shared/external/app-intents/SKILL.md +302 -304
  18. package/pipeline/skills/shared/external/app-intents/evals/evals.json +21 -21
  19. package/pipeline/skills/shared/external/app-intents/references/appintents-advanced.md +594 -894
  20. package/pipeline/skills/shared/external/app-store-optimization/SKILL.md +339 -277
  21. package/pipeline/skills/shared/external/app-store-optimization/evals/evals.json +27 -23
  22. package/pipeline/skills/shared/external/app-store-optimization/references/keyword-research-methodology.md +105 -122
  23. package/pipeline/skills/shared/external/app-store-optimization/references/product-page-variants.md +143 -166
  24. package/pipeline/skills/shared/external/app-store-review/SKILL.md +307 -326
  25. package/pipeline/skills/shared/external/app-store-review/evals/evals.json +21 -21
  26. package/pipeline/skills/shared/external/app-store-review/references/privacy-manifest.md +105 -67
  27. package/pipeline/skills/shared/external/app-store-review/references/review-checklists.md +114 -101
  28. package/pipeline/skills/shared/external/apple-on-device-ai/SKILL.md +333 -360
  29. package/pipeline/skills/shared/external/apple-on-device-ai/evals/evals.json +24 -27
  30. package/pipeline/skills/shared/external/apple-on-device-ai/references/coreml-conversion.md +215 -322
  31. package/pipeline/skills/shared/external/apple-on-device-ai/references/coreml-optimization.md +161 -256
  32. package/pipeline/skills/shared/external/apple-on-device-ai/references/foundation-models.md +277 -387
  33. package/pipeline/skills/shared/external/apple-on-device-ai/references/mlx-swift.md +196 -210
  34. package/pipeline/skills/shared/external/authentication/SKILL.md +265 -381
  35. package/pipeline/skills/shared/external/authentication/evals/evals.json +25 -25
  36. package/pipeline/skills/shared/external/authentication/references/keychain-biometric.md +133 -178
  37. package/pipeline/skills/shared/external/authentication/references/passkeys.md +111 -147
  38. package/pipeline/skills/shared/external/avkit/SKILL.md +267 -364
  39. package/pipeline/skills/shared/external/avkit/evals/evals.json +26 -26
  40. package/pipeline/skills/shared/external/avkit/references/avkit-patterns.md +375 -493
  41. package/pipeline/skills/shared/external/background-processing/SKILL.md +270 -382
  42. package/pipeline/skills/shared/external/background-processing/evals/evals.json +22 -22
  43. package/pipeline/skills/shared/external/background-processing/references/background-task-patterns.md +169 -317
  44. package/pipeline/skills/shared/external/callkit-voip/SKILL.md +290 -371
  45. package/pipeline/skills/shared/external/callkit-voip/evals/evals.json +24 -24
  46. package/pipeline/skills/shared/external/callkit-voip/references/callkit-patterns.md +175 -343
  47. package/pipeline/skills/shared/external/cloudkit-sync/SKILL.md +292 -381
  48. package/pipeline/skills/shared/external/cloudkit-sync/evals/evals.json +33 -30
  49. package/pipeline/skills/shared/external/cloudkit-sync/references/cloudkit-patterns.md +227 -355
  50. package/pipeline/skills/shared/external/contacts-framework/SKILL.md +197 -346
  51. package/pipeline/skills/shared/external/contacts-framework/evals/evals.json +19 -21
  52. package/pipeline/skills/shared/external/contacts-framework/references/contacts-patterns.md +169 -308
  53. package/pipeline/skills/shared/external/core-bluetooth/SKILL.md +226 -376
  54. package/pipeline/skills/shared/external/core-bluetooth/evals/evals.json +25 -22
  55. package/pipeline/skills/shared/external/core-bluetooth/references/ble-patterns.md +257 -337
  56. package/pipeline/skills/shared/external/core-data/SKILL.md +292 -368
  57. package/pipeline/skills/shared/external/core-data/evals/evals.json +30 -27
  58. package/pipeline/skills/shared/external/core-motion/SKILL.md +235 -324
  59. package/pipeline/skills/shared/external/core-motion/evals/evals.json +31 -27
  60. package/pipeline/skills/shared/external/core-motion/references/motion-patterns.md +210 -310
  61. package/pipeline/skills/shared/external/core-nfc/SKILL.md +292 -366
  62. package/pipeline/skills/shared/external/core-nfc/evals/evals.json +22 -24
  63. package/pipeline/skills/shared/external/core-nfc/references/nfc-patterns.md +233 -329
  64. package/pipeline/skills/shared/external/coreml/SKILL.md +323 -367
  65. package/pipeline/skills/shared/external/coreml/evals/evals.json +24 -21
  66. package/pipeline/skills/shared/external/coreml/references/coreml-swift-integration.md +562 -565
  67. package/pipeline/skills/shared/external/cryptokit/SKILL.md +253 -394
  68. package/pipeline/skills/shared/external/cryptokit/evals/evals.json +20 -18
  69. package/pipeline/skills/shared/external/cryptokit/references/cryptokit-patterns.md +299 -488
  70. package/pipeline/skills/shared/external/debugging-instruments/SKILL.md +270 -323
  71. package/pipeline/skills/shared/external/debugging-instruments/evals/evals.json +27 -30
  72. package/pipeline/skills/shared/external/debugging-instruments/references/instruments-guide.md +167 -315
  73. package/pipeline/skills/shared/external/debugging-instruments/references/lldb-patterns.md +140 -193
  74. package/pipeline/skills/shared/external/device-integrity/SKILL.md +230 -353
  75. package/pipeline/skills/shared/external/device-integrity/evals/evals.json +25 -21
  76. package/pipeline/skills/shared/external/device-integrity/references/device-integrity-patterns.md +159 -197
  77. package/pipeline/skills/shared/external/energykit/SKILL.md +225 -392
  78. package/pipeline/skills/shared/external/energykit/evals/evals.json +29 -28
  79. package/pipeline/skills/shared/external/energykit/references/energykit-patterns.md +174 -470
  80. package/pipeline/skills/shared/external/eventkit-calendar/SKILL.md +261 -383
  81. package/pipeline/skills/shared/external/eventkit-calendar/evals/evals.json +25 -22
  82. package/pipeline/skills/shared/external/eventkit-calendar/references/eventkit-patterns.md +165 -268
  83. package/pipeline/skills/shared/external/healthkit/SKILL.md +252 -303
  84. package/pipeline/skills/shared/external/healthkit/evals/evals.json +24 -23
  85. package/pipeline/skills/shared/external/healthkit/references/healthkit-patterns.md +369 -523
  86. package/pipeline/skills/shared/external/homekit-matter/SKILL.md +233 -348
  87. package/pipeline/skills/shared/external/homekit-matter/evals/evals.json +27 -22
  88. package/pipeline/skills/shared/external/homekit-matter/references/matter-commissioning.md +199 -305
  89. package/pipeline/skills/shared/external/ios-accessibility/SKILL.md +368 -340
  90. package/pipeline/skills/shared/external/ios-accessibility/evals/evals.json +28 -27
  91. package/pipeline/skills/shared/external/ios-accessibility/references/a11y-patterns.md +314 -260
  92. package/pipeline/skills/shared/external/ios-accessibility/references/media-accessibility.md +97 -67
  93. package/pipeline/skills/shared/external/ios-accessibility/references/nutrition-labels.md +165 -101
  94. package/pipeline/skills/shared/external/ios-localization/SKILL.md +258 -371
  95. package/pipeline/skills/shared/external/ios-localization/evals/evals.json +23 -23
  96. package/pipeline/skills/shared/external/ios-localization/references/formatstyle-locale.md +283 -491
  97. package/pipeline/skills/shared/external/ios-localization/references/string-catalogs.md +313 -440
  98. package/pipeline/skills/shared/external/ios-networking/SKILL.md +265 -341
  99. package/pipeline/skills/shared/external/ios-networking/evals/evals.json +24 -24
  100. package/pipeline/skills/shared/external/ios-networking/references/background-websocket.md +425 -652
  101. package/pipeline/skills/shared/external/ios-networking/references/file-storage-patterns.md +143 -285
  102. package/pipeline/skills/shared/external/ios-networking/references/lightweight-clients.md +93 -53
  103. package/pipeline/skills/shared/external/ios-networking/references/network-framework.md +231 -456
  104. package/pipeline/skills/shared/external/ios-networking/references/urlsession-patterns.md +517 -784
  105. package/pipeline/skills/shared/external/ios-simulator/SKILL.md +265 -393
  106. package/pipeline/skills/shared/external/ios-simulator/evals/evals.json +21 -21
  107. package/pipeline/skills/shared/external/ios-simulator/references/simctl-commands.md +177 -270
  108. package/pipeline/skills/shared/external/live-activities/SKILL.md +318 -360
  109. package/pipeline/skills/shared/external/live-activities/evals/evals.json +21 -21
  110. package/pipeline/skills/shared/external/live-activities/references/activitykit-patterns.md +478 -710
  111. package/pipeline/skills/shared/external/mapkit-location/SKILL.md +295 -267
  112. package/pipeline/skills/shared/external/mapkit-location/evals/evals.json +28 -24
  113. package/pipeline/skills/shared/external/mapkit-location/references/mapkit-corelocation-patterns.md +378 -532
  114. package/pipeline/skills/shared/external/mapkit-location/references/mapkit-patterns.md +397 -499
  115. package/pipeline/skills/shared/external/metrickit-diagnostics/SKILL.md +165 -348
  116. package/pipeline/skills/shared/external/metrickit-diagnostics/evals/evals.json +26 -23
  117. package/pipeline/skills/shared/external/metrickit-diagnostics/references/metrickit-patterns.md +123 -130
  118. package/pipeline/skills/shared/external/musickit-audio/SKILL.md +189 -315
  119. package/pipeline/skills/shared/external/musickit-audio/evals/evals.json +22 -21
  120. package/pipeline/skills/shared/external/musickit-audio/references/musickit-patterns.md +181 -270
  121. package/pipeline/skills/shared/external/natural-language/SKILL.md +188 -340
  122. package/pipeline/skills/shared/external/natural-language/evals/evals.json +21 -21
  123. package/pipeline/skills/shared/external/natural-language/references/translation-patterns.md +171 -225
  124. package/pipeline/skills/shared/external/passkit-wallet/SKILL.md +258 -392
  125. package/pipeline/skills/shared/external/passkit-wallet/evals/evals.json +30 -29
  126. package/pipeline/skills/shared/external/passkit-wallet/references/wallet-passes.md +164 -231
  127. package/pipeline/skills/shared/external/pdfkit/SKILL.md +312 -344
  128. package/pipeline/skills/shared/external/pdfkit/evals/evals.json +19 -19
  129. package/pipeline/skills/shared/external/pdfkit/references/pdfkit-patterns.md +413 -624
  130. package/pipeline/skills/shared/external/pencilkit-drawing/SKILL.md +242 -358
  131. package/pipeline/skills/shared/external/pencilkit-drawing/evals/evals.json +25 -21
  132. package/pipeline/skills/shared/external/pencilkit-drawing/references/pencilkit-patterns.md +161 -226
  133. package/pipeline/skills/shared/external/permissionkit/SKILL.md +282 -400
  134. package/pipeline/skills/shared/external/permissionkit/evals/evals.json +27 -30
  135. package/pipeline/skills/shared/external/permissionkit/references/permissionkit-patterns.md +237 -350
  136. package/pipeline/skills/shared/external/photos-camera-media/SKILL.md +276 -325
  137. package/pipeline/skills/shared/external/photos-camera-media/references/av-playback.md +299 -545
  138. package/pipeline/skills/shared/external/photos-camera-media/references/camera-capture.md +344 -588
  139. package/pipeline/skills/shared/external/photos-camera-media/references/image-loading-caching.md +316 -660
  140. package/pipeline/skills/shared/external/photos-camera-media/references/photokit-patterns.md +270 -416
  141. package/pipeline/skills/shared/external/push-notifications/SKILL.md +312 -340
  142. package/pipeline/skills/shared/external/push-notifications/evals/evals.json +27 -26
  143. package/pipeline/skills/shared/external/push-notifications/references/notification-patterns.md +328 -485
  144. package/pipeline/skills/shared/external/push-notifications/references/rich-notifications.md +327 -560
  145. package/pipeline/skills/shared/external/realitykit-ar/SKILL.md +218 -410
  146. package/pipeline/skills/shared/external/realitykit-ar/evals/evals.json +24 -27
  147. package/pipeline/skills/shared/external/realitykit-ar/references/realitykit-patterns.md +221 -348
  148. package/pipeline/skills/shared/external/shareplay-activities/SKILL.md +222 -393
  149. package/pipeline/skills/shared/external/shareplay-activities/evals/evals.json +23 -24
  150. package/pipeline/skills/shared/external/shareplay-activities/references/shareplay-patterns.md +280 -420
  151. package/pipeline/skills/shared/external/speech-recognition/SKILL.md +217 -421
  152. package/pipeline/skills/shared/external/speech-recognition/evals/evals.json +23 -26
  153. package/pipeline/skills/shared/external/speech-recognition/references/speechanalyzer-patterns.md +133 -125
  154. package/pipeline/skills/shared/external/storekit/SKILL.md +228 -204
  155. package/pipeline/skills/shared/external/storekit/evals/evals.json +27 -24
  156. package/pipeline/skills/shared/external/storekit/references/app-review-guidelines.md +98 -109
  157. package/pipeline/skills/shared/external/storekit/references/core-patterns.md +298 -242
  158. package/pipeline/skills/shared/external/storekit/references/storekit-advanced.md +356 -649
  159. package/pipeline/skills/shared/external/swift-api-design-guidelines/SKILL.md +274 -399
  160. package/pipeline/skills/shared/external/swift-api-design-guidelines/evals/evals.json +22 -24
  161. package/pipeline/skills/shared/external/swift-api-design-guidelines/references/argument-labels-and-parameters.md +107 -108
  162. package/pipeline/skills/shared/external/swift-api-design-guidelines/references/conventions-and-special-rules.md +93 -165
  163. package/pipeline/skills/shared/external/swift-api-design-guidelines/references/naming-and-clarity.md +99 -137
  164. package/pipeline/skills/shared/external/swift-api-design-guidelines/references/side-effects-and-mutating-pairs.md +77 -120
  165. package/pipeline/skills/shared/external/swift-architecture/SKILL.md +334 -350
  166. package/pipeline/skills/shared/external/swift-architecture/evals/evals.json +22 -22
  167. package/pipeline/skills/shared/external/swift-charts/SKILL.md +208 -394
  168. package/pipeline/skills/shared/external/swift-charts/evals/evals.json +27 -30
  169. package/pipeline/skills/shared/external/swift-charts/references/charts-patterns.md +351 -762
  170. package/pipeline/skills/shared/external/swift-codable/SKILL.md +339 -343
  171. package/pipeline/skills/shared/external/swift-codable/evals/evals.json +20 -20
  172. package/pipeline/skills/shared/external/swift-concurrency/SKILL.md +303 -351
  173. package/pipeline/skills/shared/external/swift-concurrency/evals/evals.json +27 -24
  174. package/pipeline/skills/shared/external/swift-concurrency/references/approachable-concurrency.md +65 -80
  175. package/pipeline/skills/shared/external/swift-concurrency/references/async-algorithms.md +48 -84
  176. package/pipeline/skills/shared/external/swift-concurrency/references/bridging-interop.md +134 -79
  177. package/pipeline/skills/shared/external/swift-concurrency/references/concurrency-patterns.md +145 -167
  178. package/pipeline/skills/shared/external/swift-concurrency/references/diagnostics.md +62 -50
  179. package/pipeline/skills/shared/external/swift-concurrency/references/swiftui-concurrency.md +92 -121
  180. package/pipeline/skills/shared/external/swift-concurrency/references/synchronization-primitives.md +177 -241
  181. package/pipeline/skills/shared/external/swift-formatstyle/SKILL.md +258 -234
  182. package/pipeline/skills/shared/external/swift-language/SKILL.md +342 -382
  183. package/pipeline/skills/shared/external/swift-language/evals/evals.json +24 -27
  184. package/pipeline/skills/shared/external/swift-language/references/swift-attributes-interop.md +79 -56
  185. package/pipeline/skills/shared/external/swift-language/references/swift-patterns-extended.md +297 -340
  186. package/pipeline/skills/shared/external/swift-security/SKILL.md +180 -161
  187. package/pipeline/skills/shared/external/swift-security/evals/evals.json +25 -25
  188. package/pipeline/skills/shared/external/swift-security/references/biometric-authentication.md +314 -469
  189. package/pipeline/skills/shared/external/swift-security/references/certificate-trust.md +408 -476
  190. package/pipeline/skills/shared/external/swift-security/references/common-anti-patterns.md +260 -530
  191. package/pipeline/skills/shared/external/swift-security/references/compliance-owasp-mapping.md +270 -477
  192. package/pipeline/skills/shared/external/swift-security/references/credential-storage-patterns.md +573 -571
  193. package/pipeline/skills/shared/external/swift-security/references/cryptokit-public-key.md +370 -441
  194. package/pipeline/skills/shared/external/swift-security/references/cryptokit-symmetric.md +332 -433
  195. package/pipeline/skills/shared/external/swift-security/references/keychain-access-control.md +346 -468
  196. package/pipeline/skills/shared/external/swift-security/references/keychain-fundamentals.md +352 -472
  197. package/pipeline/skills/shared/external/swift-security/references/keychain-item-classes.md +431 -432
  198. package/pipeline/skills/shared/external/swift-security/references/keychain-sharing.md +328 -425
  199. package/pipeline/skills/shared/external/swift-security/references/migration-legacy-stores.md +341 -579
  200. package/pipeline/skills/shared/external/swift-security/references/secure-enclave.md +396 -457
  201. package/pipeline/skills/shared/external/swift-security/references/testing-security-code.md +354 -614
  202. package/pipeline/skills/shared/external/swift-testing/SKILL.md +188 -175
  203. package/pipeline/skills/shared/external/swift-testing/evals/evals.json +26 -24
  204. package/pipeline/skills/shared/external/swift-testing/references/testing-advanced.md +80 -84
  205. package/pipeline/skills/shared/external/swift-testing/references/testing-patterns.md +317 -433
  206. package/pipeline/skills/shared/external/swiftdata/SKILL.md +392 -256
  207. package/pipeline/skills/shared/external/swiftdata/evals/evals.json +24 -24
  208. package/pipeline/skills/shared/external/swiftdata/references/core-data-coexistence.md +206 -402
  209. package/pipeline/skills/shared/external/swiftdata/references/indexing.md +59 -52
  210. package/pipeline/skills/shared/external/swiftdata/references/predicate-pitfalls.md +57 -33
  211. package/pipeline/skills/shared/external/swiftdata/references/swiftdata-advanced.md +354 -747
  212. package/pipeline/skills/shared/external/swiftdata/references/swiftdata-queries.md +300 -508
  213. package/pipeline/skills/shared/external/swiftlint/SKILL.md +175 -226
  214. package/pipeline/skills/shared/external/swiftlint/references/adoption-and-configuration.md +141 -208
  215. package/pipeline/skills/shared/external/swiftlint/references/custom-rules-and-analyze.md +100 -109
  216. package/pipeline/skills/shared/external/swiftlint/references/plugins-run-scripts-and-integrations.md +159 -179
  217. package/pipeline/skills/shared/external/swiftlint/references/rule-reference.md +383 -18
  218. package/pipeline/skills/shared/external/swiftlint/references/rules-suppressions-and-baselines.md +143 -229
  219. package/pipeline/skills/shared/external/swiftui-animation/SKILL.md +283 -366
  220. package/pipeline/skills/shared/external/swiftui-animation/references/animation-advanced.md +396 -608
  221. package/pipeline/skills/shared/external/swiftui-animation/references/core-animation-bridge.md +336 -385
  222. package/pipeline/skills/shared/external/swiftui-gestures/SKILL.md +239 -349
  223. package/pipeline/skills/shared/external/swiftui-gestures/references/gesture-patterns.md +228 -310
  224. package/pipeline/skills/shared/external/swiftui-layout-components/SKILL.md +260 -249
  225. package/pipeline/skills/shared/external/swiftui-layout-components/references/form.md +92 -74
  226. package/pipeline/skills/shared/external/swiftui-layout-components/references/grids.md +112 -177
  227. package/pipeline/skills/shared/external/swiftui-layout-components/references/list.md +61 -64
  228. package/pipeline/skills/shared/external/swiftui-layout-components/references/scrollview.md +94 -134
  229. package/pipeline/skills/shared/external/swiftui-liquid-glass/SKILL.md +193 -225
  230. package/pipeline/skills/shared/external/swiftui-liquid-glass/references/liquid-glass.md +173 -327
  231. package/pipeline/skills/shared/external/swiftui-navigation/SKILL.md +193 -168
  232. package/pipeline/skills/shared/external/swiftui-navigation/references/deeplinks.md +127 -150
  233. package/pipeline/skills/shared/external/swiftui-navigation/references/navigationstack.md +132 -133
  234. package/pipeline/skills/shared/external/swiftui-navigation/references/sheets.md +152 -117
  235. package/pipeline/skills/shared/external/swiftui-navigation/references/tabview.md +106 -140
  236. package/pipeline/skills/shared/external/swiftui-patterns/SKILL.md +316 -252
  237. package/pipeline/skills/shared/external/swiftui-patterns/references/architecture-patterns.md +341 -332
  238. package/pipeline/skills/shared/external/swiftui-patterns/references/deprecated-migration.md +547 -854
  239. package/pipeline/skills/shared/external/swiftui-patterns/references/design-polish.md +485 -537
  240. package/pipeline/skills/shared/external/swiftui-patterns/references/platform-and-sharing.md +417 -499
  241. package/pipeline/skills/shared/external/swiftui-performance/SKILL.md +213 -376
  242. package/pipeline/skills/shared/external/swiftui-performance/references/demystify-swiftui-performance-wwdc23.md +86 -175
  243. package/pipeline/skills/shared/external/swiftui-performance/references/optimizing-swiftui-performance-instruments.md +89 -195
  244. package/pipeline/skills/shared/external/swiftui-performance/references/understanding-hangs-in-your-app.md +95 -182
  245. package/pipeline/skills/shared/external/swiftui-performance/references/understanding-improving-swiftui-performance.md +71 -149
  246. package/pipeline/skills/shared/external/swiftui-performance/references/wwdc-session-sources.md +21 -27
  247. package/pipeline/skills/shared/external/swiftui-uikit-interop/SKILL.md +303 -295
  248. package/pipeline/skills/shared/external/swiftui-uikit-interop/references/hosting-migration.md +204 -387
  249. package/pipeline/skills/shared/external/swiftui-uikit-interop/references/representable-recipes.md +469 -683
  250. package/pipeline/skills/shared/external/swiftui-webkit/SKILL.md +140 -186
  251. package/pipeline/skills/shared/external/swiftui-webkit/references/loading-and-observation.md +75 -86
  252. package/pipeline/skills/shared/external/swiftui-webkit/references/local-content-and-custom-schemes.md +63 -60
  253. package/pipeline/skills/shared/external/swiftui-webkit/references/migration-and-fallbacks.md +69 -137
  254. package/pipeline/skills/shared/external/swiftui-webkit/references/navigation-and-javascript.md +95 -67
  255. package/pipeline/skills/shared/external/tipkit/SKILL.md +220 -335
  256. package/pipeline/skills/shared/external/tipkit/references/tipkit-patterns.md +356 -494
  257. package/pipeline/skills/shared/external/vision-framework/SKILL.md +260 -375
  258. package/pipeline/skills/shared/external/vision-framework/references/vision-requests.md +393 -515
  259. package/pipeline/skills/shared/external/vision-framework/references/visionkit-scanner.md +363 -539
  260. package/pipeline/skills/shared/external/weatherkit/SKILL.md +152 -310
  261. package/pipeline/skills/shared/external/weatherkit/references/weatherkit-patterns.md +288 -407
  262. package/pipeline/skills/shared/external/widgetkit/SKILL.md +216 -288
  263. package/pipeline/skills/shared/external/widgetkit/references/widgetkit-advanced.md +414 -719
  264. package/pipeline/skills/shared/external/NOTICE-swift-ios-skills.md +0 -39
@@ -1,620 +1,500 @@
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)
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 17+ / macOS 14+)](#an-actor-around-the-keychain-ios-17--macos-14)
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)
36
27
 
37
- ## Architecture Overview
28
+ ## How the Keychain Is Built
38
29
 
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`.
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`.
40
34
 
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.
35
+ Items are protected in two tiers, both AES-256-GCM:
42
36
 
43
- ---
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.
44
41
 
45
- ## The Four Functions and Their Dictionary Contracts
42
+ That split explains most of the performance advice further down.
46
43
 
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:
44
+ ## Dictionary Contracts
48
45
 
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`
46
+ Quinn groups the keys you can put in a SecItem dictionary into five kinds:
54
47
 
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 |
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` |
61
55
 
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.
56
+ Each function accepts a different subset (all available since iOS 2.0):
63
57
 
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."
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` |
65
64
 
66
- ---
65
+ The delete default deserves attention: a query that names only the class, or
66
+ only the service, removes every item it matches.
67
67
 
68
- ## Uniqueness and Primary Keys
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).
69
71
 
70
- For `kSecClassGenericPassword`, uniqueness is determined by the combination of:
72
+ ## Uniqueness
71
73
 
72
- - `kSecAttrAccount` + `kSecAttrService` + `kSecAttrAccessGroup` + `kSecAttrSynchronizable`
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.
73
78
 
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`.
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`.
75
83
 
76
- For `kSecClassInternetPassword`, the uniqueness set includes: `kSecAttrAccount` + `kSecAttrServer` + `kSecAttrProtocol` + `kSecAttrAuthenticationType` + `kSecAttrPort` + `kSecAttrPath` + `kSecAttrSecurityDomain` + `kSecAttrAccessGroup` + `kSecAttrSynchronizable`.
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.
77
87
 
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`).
88
+ `SecItemUpdate` cannot change `kSecAttrAccount` or `kSecClass`. To rename an
89
+ account or change class, delete the old item and add a new one.
79
90
 
80
- ---
91
+ ## Saving: Add, Then Update on Duplicate
81
92
 
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:**
93
+ The bug that shows up most in generated keychain code is a `SecItemAdd` whose
94
+ result is never read. The first save works; the second returns
95
+ `errSecDuplicateItem` (-25299), nothing is written, and the app keeps running
96
+ with the stale value.
87
97
 
88
98
  ```swift
89
- // ❌ WRONG - silently fails if item already exists
90
- func savePassword(_ password: String, account: String) {
91
- let query: [CFString: Any] = [
99
+ // Wrong: status ignored, so every save after the first silently does nothing.
100
+ func storeWrong(_ pin: Data) {
101
+ let item: [CFString: Any] = [
92
102
  kSecClass: kSecClassGenericPassword,
93
- kSecAttrService: "com.example.app",
94
- kSecAttrAccount: account,
95
- kSecValueData: Data(password.utf8)
103
+ kSecAttrService: "com.example.lockbox",
104
+ kSecAttrAccount: "vault-pin",
105
+ kSecValueData: pin
96
106
  ]
97
- SecItemAdd(query as CFDictionary, nil) // Return value IGNORED!
98
- // If item exists → errSecDuplicateItem (-25299) - password never saved
107
+ SecItemAdd(item as CFDictionary, nil)
99
108
  }
100
109
  ```
101
110
 
102
- ✅ **Correct add-or-update with exhaustive OSStatus handling:**
103
-
104
111
  ```swift
105
- // ✅ CORRECT - attempts add, falls back to update on duplicate
106
- func savePassword(_ password: String, account: String) throws {
107
- let baseQuery: [CFString: Any] = [
112
+ func store(_ pin: Data) throws {
113
+ let identity: [CFString: Any] = [
108
114
  kSecClass: kSecClassGenericPassword,
109
- kSecAttrService: "com.example.app",
110
- kSecAttrAccount: account
115
+ kSecAttrService: "com.example.lockbox",
116
+ kSecAttrAccount: "vault-pin"
111
117
  ]
118
+ var insert = identity
119
+ insert[kSecValueData] = pin
120
+ insert[kSecAttrAccessible] = kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
112
121
 
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 {
122
+ let added = SecItemAdd(insert as CFDictionary, nil)
123
+ switch added {
120
124
  case errSecSuccess:
121
125
  return
122
-
123
126
  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
-
127
+ let changes: [CFString: Any] = [kSecValueData: pin]
128
+ let updated = SecItemUpdate(identity as CFDictionary, changes as CFDictionary)
129
+ guard updated == errSecSuccess else { throw KeychainError(status: updated) }
134
130
  case errSecInteractionNotAllowed:
135
- // Device locked - do NOT delete-and-retry!
136
- throw KeychainError(status: addStatus)
137
-
131
+ throw KeychainError(status: added)
138
132
  default:
139
- throw KeychainError(status: addStatus)
133
+ throw KeychainError(status: added)
140
134
  }
141
135
  }
142
136
  ```
143
137
 
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.
138
+ Points to carry into any variant:
149
139
 
150
- ---
140
+ - The changes dictionary lists only what should change. It never contains
141
+ `kSecClass` or the search attributes.
142
+ - `errSecInteractionNotAllowed` (-25308) tells you the item exists but cannot be
143
+ opened now: the device is locked, or the item needs UI that cannot appear. The item is fine. Deleting it here destroys
144
+ good data.
145
+ - Updating in place beats deleting and re-adding: persistent references to the
146
+ item stay valid, and there is no moment where the item is missing for a
147
+ concurrent reader. Delete and re-add only when the access control itself has
148
+ to change (see [keychain-access-control.md](keychain-access-control.md)).
151
149
 
152
- ## Reading from the Keychain: Return Flags and Type Casting
150
+ ## Reading
153
151
 
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.
155
-
156
- ❌ **Query that returns no data because `kSecReturnData` is missing:**
152
+ Next in frequency: a `SecItemCopyMatching` that sets no `kSecReturn*` key. The call succeeds and the result is empty.
157
153
 
158
154
  ```swift
159
- // ❌ WRONG - no kSecReturn* flags, result is always nil
160
- func loadPassword(account: String) -> Data? {
161
- let query: [CFString: Any] = [
155
+ // Wrong: no return key, so `out` is nil and the cast always fails.
156
+ func fetchWrong() -> Data? {
157
+ let lookup: [CFString: Any] = [
162
158
  kSecClass: kSecClassGenericPassword,
163
- kSecAttrService: "com.example.app",
164
- kSecAttrAccount: account,
159
+ kSecAttrService: "com.example.lockbox",
160
+ kSecAttrAccount: "vault-pin",
165
161
  kSecMatchLimit: kSecMatchLimitOne
166
- // BUG: Missing kSecReturnData: true
167
162
  ]
168
- var result: CFTypeRef?
169
- SecItemCopyMatching(query as CFDictionary, &result)
170
- return result as? Data // Always nil - no return type was requested
163
+ var out: CFTypeRef?
164
+ SecItemCopyMatching(lookup as CFDictionary, &out)
165
+ return out as? Data
171
166
  }
172
167
  ```
173
168
 
174
- ✅ **Correct query with proper return flags and exhaustive error handling:**
175
-
176
169
  ```swift
177
- // ✅ CORRECT - explicitly requests data, handles all error states
178
- func loadPassword(account: String) throws -> Data? {
179
- let query: [CFString: Any] = [
170
+ func fetch() throws -> Data? {
171
+ let lookup: [CFString: Any] = [
180
172
  kSecClass: kSecClassGenericPassword,
181
- kSecAttrService: "com.example.app",
182
- kSecAttrAccount: account,
173
+ kSecAttrService: "com.example.lockbox",
174
+ kSecAttrAccount: "vault-pin",
183
175
  kSecMatchLimit: kSecMatchLimitOne,
184
- kSecReturnData: true // ← REQUIRED to get the secret
176
+ kSecReturnData: true
185
177
  ]
186
-
187
- var result: CFTypeRef?
188
- let status = SecItemCopyMatching(query as CFDictionary, &result)
189
-
178
+ var out: CFTypeRef?
179
+ let status = SecItemCopyMatching(lookup as CFDictionary, &out)
190
180
  switch status {
191
181
  case errSecSuccess:
192
- guard let data = result as? Data else {
193
- throw KeychainError(status: errSecParam)
194
- }
195
- return data
196
-
182
+ guard let bytes = out as? Data else { throw KeychainError(status: errSecParam) }
183
+ return bytes
197
184
  case errSecItemNotFound:
198
- return nil // Legitimate "not found" - not an error
199
-
185
+ return nil
200
186
  case errSecInteractionNotAllowed:
201
187
  throw KeychainError(status: status)
202
-
203
188
  default:
204
189
  throw KeychainError(status: status)
205
190
  }
206
191
  }
207
192
  ```
208
193
 
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)
194
+ What lands in `out` depends on the return keys and the match limit:
232
195
 
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
- ```
196
+ | Return keys | `kSecMatchLimitOne` gives | `kSecMatchLimitAll` gives |
197
+ | --- | --- | --- |
198
+ | Data | `Data` | array of `Data` |
199
+ | Attributes | `[String: Any]` | array of dictionaries |
200
+ | Data + Attributes | dictionary, secret under `kSecValueData` | array of dictionaries |
201
+ | Ref | `SecKey`, `SecCertificate` or `SecIdentity` | array of refs |
202
+ | PersistentRef | opaque `Data` | array of `Data` |
237
203
 
238
- Both are correct. Pick one style and use it consistently across your codebase.
204
+ Some OS versions refuse `kSecReturnData` combined with `kSecMatchLimitAll` for
205
+ password classes. List items with attributes or refs, then fetch data for the
206
+ one you need.
239
207
 
240
- ---
208
+ Use the `kSec*` constants, never their string values such as `"svce"` or
209
+ `"class"`. Dictionaries typed `[CFString: Any]` work, and so do `[String: Any]` ones
210
+ whose keys are cast with `as String`; pick one style per codebase. This file uses `[CFString: Any]` because it is
211
+ shorter.
241
212
 
242
- ## Centralized Query Builder
213
+ ## One Place That Builds Queries
243
214
 
244
- Centralize query construction to prevent flag omissions and key typos:
215
+ A small builder keeps every query shape in one auditable spot, so a missing
216
+ return flag or a typo in an attribute shows up in one review instead of twenty.
245
217
 
246
218
  ```swift
247
- enum KeychainQueryBuilder {
248
- static func buildQuery(
249
- forClass secClass: CFString = kSecClassGenericPassword,
250
- account: String? = nil,
251
- service: String? = nil,
219
+ enum KeychainQuery {
220
+ static func make(
221
+ itemClass: CFString = kSecClassGenericPassword,
222
+ account: String,
223
+ service: String,
252
224
  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
225
+ wantData: Bool = false,
226
+ wantAttributes: Bool = false,
227
+ limit: CFString = kSecMatchLimitOne
228
+ ) -> [CFString: Any] {
229
+ var q: [CFString: Any] = [
230
+ kSecClass: itemClass,
231
+ kSecAttrAccount: account,
232
+ kSecAttrService: service,
233
+ kSecMatchLimit: limit
234
+ ]
235
+ if let accessGroup { q[kSecAttrAccessGroup] = accessGroup }
236
+ if wantData { q[kSecReturnData] = true }
237
+ if wantAttributes { q[kSecReturnAttributes] = true }
238
+ return q
267
239
  }
268
240
  }
269
241
  ```
270
242
 
271
- This pattern ensures return flags are set deliberately and provides a single site to audit query construction.
243
+ Drop `kSecMatchLimit` and the return flags before handing a builder result to
244
+ `SecItemAdd`, `SecItemUpdate` or `SecItemDelete`.
272
245
 
273
- ---
246
+ ## OSStatus Codes
274
247
 
275
- ## OSStatus Error Handling
248
+ A nonzero status is not automatically a failure to report. Treat each one on
249
+ its merits:
276
250
 
277
- Never treat all non-zero `OSStatus` values as fatal errors. Several codes represent expected operational states:
278
-
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:
251
+ | Code | Constant | Response |
252
+ | --- | --- | --- |
253
+ | 0 | `errSecSuccess` | Continue. |
254
+ | -25299 | `errSecDuplicateItem` | Switch to `SecItemUpdate`. |
255
+ | -25300 | `errSecItemNotFound` | Return nil; on delete, count it as success. |
256
+ | -25308 | `errSecInteractionNotAllowed` | Try again later (device locked). Never delete. |
257
+ | -128 | `errSecUserCanceled` | The user dismissed the prompt; tell the UI. |
258
+ | -25293 | `errSecAuthFailed` | Authentication failed or the protected item is no longer usable. |
259
+ | -50 | `errSecParam` | A programming error in the dictionary; fix the query. |
260
+ | -25244 | `errSecNoSuchAttr` | The attribute is not supported (seen on the data protection keychain). |
290
261
 
291
262
  ```swift
292
263
  struct KeychainError: Error, CustomStringConvertible {
293
264
  let status: OSStatus
294
-
295
265
  var description: String {
296
- let msg = SecCopyErrorMessageString(status, nil) as String? ?? "Unknown"
297
- return "KeychainError(\(status)): \(msg)"
266
+ let text = SecCopyErrorMessageString(status, nil) as String? ?? "unknown"
267
+ return "Keychain status \(status): \(text)"
298
268
  }
299
269
  }
300
270
  ```
301
271
 
302
- **Logging safety:** Log only the query shape and resulting status code. Never log secret data (`kSecValueData`), tokens, or keys.
272
+ Logs may record the shape of a query (class, service, which flags were set)
273
+ and the status code. They never record `kSecValueData`, tokens or key bytes.
303
274
 
304
- ---
275
+ ## An Actor Around the Keychain (iOS 17+ / macOS 14+)
305
276
 
306
- ## Actor-Isolated Keychain Manager (iOS 17+ / macOS 14+)
307
-
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).
309
-
310
- ❌ **`@MainActor` keychain access that blocks the UI:**
277
+ Reads of biometric-protected items wait for the user and can take several
278
+ seconds (WWDC 2014 Session 711). Doing that on the main actor freezes the UI.
311
279
 
312
280
  ```swift
313
- // ❌ WRONG - blocks main thread, freezes UI during securityd IPC
281
+ // Wrong: the main actor waits on securityd and, for protected items, on the user.
314
282
  @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
283
+ final class ProfileModel: ObservableObject {
284
+ @Published var apiKey: String?
285
+ func load() {
286
+ var out: CFTypeRef?
287
+ let q: [CFString: Any] = [kSecClass: kSecClassGenericPassword,
288
+ kSecAttrAccount: "api", kSecReturnData: true]
289
+ _ = SecItemCopyMatching(q as CFDictionary, &out)
290
+ apiKey = (out as? Data).flatMap { String(decoding: $0, as: UTF8.self) }
332
291
  }
333
292
  }
334
293
  ```
335
294
 
336
- ✅ **Actor-isolated keychain manager with full CRUD:**
337
-
338
295
  ```swift
339
- // ✅ CORRECT - dedicated actor keeps all SecItem calls off @MainActor
340
- actor KeychainManager {
341
- static let shared = KeychainManager()
342
-
296
+ actor SecretStore {
297
+ static let shared = SecretStore()
343
298
  private let service: String
344
299
 
345
- init(service: String = Bundle.main.bundleIdentifier ?? "default") {
300
+ init(service: String = Bundle.main.bundleIdentifier ?? "app.secrets") {
346
301
  self.service = service
347
302
  }
348
303
 
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
- }
304
+ private func identity(_ account: String) -> [CFString: Any] {
305
+ [kSecClass: kSecClassGenericPassword,
306
+ kSecAttrService: service,
307
+ kSecAttrAccount: account]
383
308
  }
384
309
 
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
- }
310
+ func put(_ value: Data, for account: String,
311
+ accessible: CFString = kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly) throws {
312
+ var insert = identity(account)
313
+ insert[kSecValueData] = value
314
+ insert[kSecAttrAccessible] = accessible
315
+ let status = SecItemAdd(insert as CFDictionary, nil)
316
+ if status == errSecSuccess { return }
317
+ guard status == errSecDuplicateItem else { throw KeychainError(status: status) }
318
+ let changed = SecItemUpdate(identity(account) as CFDictionary,
319
+ [kSecValueData: value] as CFDictionary)
320
+ guard changed == errSecSuccess else { throw KeychainError(status: changed) }
409
321
  }
410
322
 
411
- // MARK: - Delete (idempotent)
323
+ func get(_ account: String) throws -> Data? {
324
+ var q = identity(account)
325
+ q[kSecReturnData] = true
326
+ q[kSecMatchLimit] = kSecMatchLimitOne
327
+ var out: CFTypeRef?
328
+ let status = SecItemCopyMatching(q as CFDictionary, &out)
329
+ if status == errSecItemNotFound { return nil }
330
+ guard status == errSecSuccess else { throw KeychainError(status: status) }
331
+ return out as? Data
332
+ }
412
333
 
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)
334
+ func remove(_ account: String) throws {
335
+ let status = SecItemDelete(identity(account) as CFDictionary)
420
336
  guard status == errSecSuccess || status == errSecItemNotFound else {
421
337
  throw KeychainError(status: status)
422
338
  }
423
339
  }
424
340
 
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:
341
+ func accounts() throws -> [String] {
342
+ let q: [CFString: Any] = [kSecClass: kSecClassGenericPassword,
343
+ kSecAttrService: service,
344
+ kSecMatchLimit: kSecMatchLimitAll,
345
+ kSecReturnAttributes: true]
346
+ var out: CFTypeRef?
347
+ let status = SecItemCopyMatching(q as CFDictionary, &out)
348
+ if status == errSecItemNotFound { return [] }
349
+ guard status == errSecSuccess, let rows = out as? [[String: Any]] else {
445
350
  throw KeychainError(status: status)
446
351
  }
352
+ return rows.compactMap { $0[kSecAttrAccount as String] as? String }
447
353
  }
448
354
  }
449
355
  ```
450
356
 
451
- **Calling from SwiftUI:**
357
+ `remove` is idempotent. `accounts()` asks for attributes only, so it never pays
358
+ for the Secure Enclave unwrap of each secret.
359
+
360
+ From SwiftUI, the view model awaits the actor and the main actor is free while
361
+ the keychain works:
452
362
 
453
363
  ```swift
454
364
  @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
- }
365
+ @Observable
366
+ final class SettingsModel {
367
+ var hasKey = false
368
+ func refresh() async {
369
+ hasKey = (try? await SecretStore.shared.get("api")) != nil
466
370
  }
467
371
  }
468
372
  ```
469
373
 
470
- ### Why Actors over GCD
374
+ Why an actor rather than a queue:
471
375
 
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 |
376
+ | | Actor | GCD serial queue |
377
+ | --- | --- | --- |
378
+ | Isolation | Checked by the compiler | Up to the developer |
379
+ | Serialization | Done by the runtime | Done by the queue, if used consistently |
380
+ | Control flow | Straight-line `async`/`await` | Nested completion handlers |
381
+ | `Sendable` | Enforced | Annotate `@Sendable` by hand |
382
+ | Swift 6 | Native | Needs care to pass strict checking |
479
383
 
480
- ### Legacy GCD Pattern (iOS 13-16 codebases)
384
+ For iOS 13 through 16, a private serial queue does the same job:
481
385
 
482
386
  ```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) {
387
+ final class LegacySecretStore {
388
+ private let queue = DispatchQueue(label: "secrets.io", qos: .userInitiated)
389
+ func get(_ account: String, completion: @escaping @Sendable (Result<Data?, Error>) -> Void) {
488
390
  queue.async {
489
- // ... SecItemCopyMatching on background queue ...
391
+ let result = Result { try readItem(account: account) }
490
392
  DispatchQueue.main.async { completion(result) }
491
393
  }
492
394
  }
493
395
  }
494
396
  ```
495
397
 
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.
398
+ Here `readItem(account:)` stands for a synchronous read such as `fetch()` in
399
+ the Reading section.
400
+ The completion is `@Sendable` because it crosses two queues; without it Swift 6
401
+ reports sending `completion` as a data race.
402
+
403
+ ## Performance
404
+
405
+ - Returning attributes only uses the cached metadata key. It is quick and needs
406
+ no Secure Enclave work.
407
+ - Returning data needs the per-row key from the Secure Enclave, so it costs
408
+ more.
409
+ - For a list screen, fetch attributes or refs, and fetch data only for the row
410
+ the user picks.
411
+ - A query with nothing but `kSecClass` and `kSecMatchLimitAll` scans the whole
412
+ table. Adding service and account lets `securityd` use its index.
413
+ - At launch every read costs IPC plus enclave latency, and items protected with
414
+ a `WhenUnlocked` class can be unavailable if the app was launched before first
415
+ unlock (background refresh, VoIP pushes).
416
+ - Do not call SecItem synchronously in
417
+ `application(_:didFinishLaunchingWithOptions:)`. Read the item when a feature
418
+ first needs it.
419
+ - Passwords cannot be written in bulk. `SecItemAdd` with `kSecUseItemList` can
420
+ add several certificates or keys at once, but not password items. Reading in
421
+ bulk means `kSecMatchLimitAll`.
422
+
423
+ ## macOS: Two Keychains (TN3137)
424
+
425
+ A Mac has two keychain implementations:
426
+
427
+ - the legacy **file-based keychain**, which SecItem uses unless told otherwise;
428
+ - the **data protection keychain**, which matches iOS.
429
+
430
+ The file-based one ignores attributes it does not understand without saying
431
+ so, applies `kSecMatchLimit` defaults inconsistently, and returns different
432
+ types from `SecItemAdd`.
433
+
434
+ Code opts into the data protection keychain with
435
+ `kSecUseDataProtectionKeychain: true`, available from macOS 10.15, or by setting
436
+ `kSecAttrSynchronizable: true`. It is required for iCloud Keychain sync,
437
+ biometric protection and storing Secure Enclave keys. New code should always
438
+ target it. Mac Catalyst apps and iOS apps running on a Mac use it
439
+ automatically.
538
440
 
539
441
  ```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
442
+ func macSafe(_ query: [CFString: Any]) -> [CFString: Any] {
443
+ var q = query
444
+ #if os(macOS)
445
+ q[kSecUseDataProtectionKeychain] = true
446
+ #endif
447
+ return q
448
+ }
549
449
  ```
550
450
 
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
451
+ When a macOS keychain bug appears, first confirm which backend the call went
452
+ to. An attribute the file-based keychain quietly drops will produce
453
+ `errSecNoSuchAttr` (-25244) on the data protection keychain.
454
+
455
+ ## Accessibility at a Glance
456
+
457
+ | Constant | Readable | In backups | Typical use |
458
+ | --- | --- | --- | --- |
459
+ | `kSecAttrAccessibleWhenUnlockedThisDeviceOnly` | while unlocked | no | the default for most secrets |
460
+ | `kSecAttrAccessibleAfterFirstUnlock` | from first unlock until restart | yes | tokens read in the background |
461
+ | `kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly` | unlocked and a passcode is set | no | the most sensitive data (OWASP recommendation) |
462
+ | `kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly` | after the first unlock, until reboot | no | device-bound items read in the background |
463
+
464
+ `kSecAttrAccessibleAlways` was deprecated in iOS 12 and is not supported on
465
+ Apple silicon Macs. Do not use it. The full model is in
466
+ [keychain-access-control.md](keychain-access-control.md).
467
+
468
+ Choosing among them: `WhenPasscodeSetThisDeviceOnly` is the strongest, but its
469
+ items are deleted if the user removes the passcode. `WhenUnlockedThisDeviceOnly`
470
+ is a safe default for foreground use. The actor above defaults to
471
+ `AfterFirstUnlockThisDeviceOnly` because it assumes background callers.
472
+
473
+ ## Checklist
474
+
475
+ 1. Every SecItem call switches over its status, covering success, duplicate,
476
+ not found and interaction-not-allowed.
477
+ 2. Saves fall back to `SecItemUpdate` on -25299.
478
+ 3. Every `SecItemCopyMatching` sets at least one `kSecReturn*` key.
479
+ 4. The cast of the `CFTypeRef` result matches the return keys and match limit.
480
+ 5. No SecItem call runs on `@MainActor` (actor on iOS 17+, serial queue on
481
+ iOS 13 to 16).
482
+ 6. Add, query and update dictionaries are built fresh for each call.
483
+ 7. Keys are `kSec*` constants, not string literals.
484
+ 8. Queries are specific: service and account for generic passwords,
485
+ `kSecMatchLimitOne` unless listing.
486
+ 9. Delete treats `errSecItemNotFound` as success.
487
+ 10. macOS code sets `kSecUseDataProtectionKeychain: true`.
488
+ 11. `errSecInteractionNotAllowed` never leads to deletion.
489
+
490
+ ## Related Files
491
+
492
+ - [keychain-item-classes.md](keychain-item-classes.md), [keychain-access-control.md](keychain-access-control.md),
493
+ [biometric-authentication.md](biometric-authentication.md), [secure-enclave.md](secure-enclave.md)
494
+ - [credential-storage-patterns.md](credential-storage-patterns.md), [keychain-sharing.md](keychain-sharing.md),
495
+ [testing-security-code.md](testing-security-code.md), [common-anti-patterns.md](common-anti-patterns.md)
496
+
497
+ Further reading: the Keychain Services documentation; TN3137; Quinn's two SecItem
498
+ forum posts; the keychain data protection chapter of the Apple Platform
499
+ Security Guide; WWDC 2014 session 711, titled Keychain and Authentication
500
+ with Touch ID; WWDC 2019 session 516, titled What's New in Authentication.