@mmerterden/multi-agent-pipeline 20.6.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 (326) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/LICENSE +0 -10
  3. package/docs/facts.json +1 -1
  4. package/manifest.json +328 -329
  5. package/package.json +2 -2
  6. package/pipeline/multi-agent-refs/features/design-conformance.md +62 -64
  7. package/pipeline/scripts/_notices.mjs +12 -1
  8. package/pipeline/scripts/gen-skills-index.mjs +1 -1
  9. package/pipeline/skills/.skill-manifest.json +106 -106
  10. package/pipeline/skills/shared/README.md +71 -71
  11. package/pipeline/skills/shared/external/agent-introspection-debugging/SKILL.md +1 -0
  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/android-architecture/SKILL.md +2 -0
  16. package/pipeline/skills/shared/external/android-performance/SKILL.md +2 -0
  17. package/pipeline/skills/shared/external/android-security/SKILL.md +2 -0
  18. package/pipeline/skills/shared/external/app-clips/SKILL.md +260 -160
  19. package/pipeline/skills/shared/external/app-clips/evals/evals.json +27 -27
  20. package/pipeline/skills/shared/external/app-clips/references/data-handoff-notifications-location.md +150 -83
  21. package/pipeline/skills/shared/external/app-clips/references/routing-and-experiences.md +135 -83
  22. package/pipeline/skills/shared/external/app-clips/references/size-capabilities-and-promotion.md +143 -85
  23. package/pipeline/skills/shared/external/app-intents/SKILL.md +302 -304
  24. package/pipeline/skills/shared/external/app-intents/evals/evals.json +21 -21
  25. package/pipeline/skills/shared/external/app-intents/references/appintents-advanced.md +594 -894
  26. package/pipeline/skills/shared/external/app-store-optimization/SKILL.md +339 -277
  27. package/pipeline/skills/shared/external/app-store-optimization/evals/evals.json +27 -23
  28. package/pipeline/skills/shared/external/app-store-optimization/references/keyword-research-methodology.md +105 -122
  29. package/pipeline/skills/shared/external/app-store-optimization/references/product-page-variants.md +143 -166
  30. package/pipeline/skills/shared/external/app-store-review/SKILL.md +307 -326
  31. package/pipeline/skills/shared/external/app-store-review/evals/evals.json +21 -21
  32. package/pipeline/skills/shared/external/app-store-review/references/privacy-manifest.md +105 -67
  33. package/pipeline/skills/shared/external/app-store-review/references/review-checklists.md +114 -101
  34. package/pipeline/skills/shared/external/apple-on-device-ai/SKILL.md +333 -360
  35. package/pipeline/skills/shared/external/apple-on-device-ai/evals/evals.json +24 -27
  36. package/pipeline/skills/shared/external/apple-on-device-ai/references/coreml-conversion.md +215 -322
  37. package/pipeline/skills/shared/external/apple-on-device-ai/references/coreml-optimization.md +161 -256
  38. package/pipeline/skills/shared/external/apple-on-device-ai/references/foundation-models.md +277 -387
  39. package/pipeline/skills/shared/external/apple-on-device-ai/references/mlx-swift.md +196 -210
  40. package/pipeline/skills/shared/external/authentication/SKILL.md +265 -381
  41. package/pipeline/skills/shared/external/authentication/evals/evals.json +25 -25
  42. package/pipeline/skills/shared/external/authentication/references/keychain-biometric.md +133 -178
  43. package/pipeline/skills/shared/external/authentication/references/passkeys.md +111 -147
  44. package/pipeline/skills/shared/external/avkit/SKILL.md +267 -364
  45. package/pipeline/skills/shared/external/avkit/evals/evals.json +26 -26
  46. package/pipeline/skills/shared/external/avkit/references/avkit-patterns.md +375 -493
  47. package/pipeline/skills/shared/external/background-processing/SKILL.md +270 -382
  48. package/pipeline/skills/shared/external/background-processing/evals/evals.json +22 -22
  49. package/pipeline/skills/shared/external/background-processing/references/background-task-patterns.md +169 -317
  50. package/pipeline/skills/shared/external/backlog/BACKLOG.md +1 -1
  51. package/pipeline/skills/shared/external/backlog/SKILL.md +56 -33
  52. package/pipeline/skills/shared/external/callkit-voip/SKILL.md +290 -371
  53. package/pipeline/skills/shared/external/callkit-voip/evals/evals.json +24 -24
  54. package/pipeline/skills/shared/external/callkit-voip/references/callkit-patterns.md +175 -343
  55. package/pipeline/skills/shared/external/ci-cd-pipelines/SKILL.md +1 -0
  56. package/pipeline/skills/shared/external/cloudkit-sync/SKILL.md +292 -381
  57. package/pipeline/skills/shared/external/cloudkit-sync/evals/evals.json +33 -30
  58. package/pipeline/skills/shared/external/cloudkit-sync/references/cloudkit-patterns.md +227 -355
  59. package/pipeline/skills/shared/external/compose-components/SKILL.md +2 -0
  60. package/pipeline/skills/shared/external/compose-navigation/SKILL.md +3 -2
  61. package/pipeline/skills/shared/external/compose-testing/SKILL.md +2 -0
  62. package/pipeline/skills/shared/external/contacts-framework/SKILL.md +197 -346
  63. package/pipeline/skills/shared/external/contacts-framework/evals/evals.json +19 -21
  64. package/pipeline/skills/shared/external/contacts-framework/references/contacts-patterns.md +169 -308
  65. package/pipeline/skills/shared/external/core-bluetooth/SKILL.md +226 -376
  66. package/pipeline/skills/shared/external/core-bluetooth/evals/evals.json +25 -22
  67. package/pipeline/skills/shared/external/core-bluetooth/references/ble-patterns.md +257 -337
  68. package/pipeline/skills/shared/external/core-data/SKILL.md +292 -368
  69. package/pipeline/skills/shared/external/core-data/evals/evals.json +30 -27
  70. package/pipeline/skills/shared/external/core-motion/SKILL.md +235 -324
  71. package/pipeline/skills/shared/external/core-motion/evals/evals.json +31 -27
  72. package/pipeline/skills/shared/external/core-motion/references/motion-patterns.md +210 -310
  73. package/pipeline/skills/shared/external/core-nfc/SKILL.md +292 -366
  74. package/pipeline/skills/shared/external/core-nfc/evals/evals.json +22 -24
  75. package/pipeline/skills/shared/external/core-nfc/references/nfc-patterns.md +233 -329
  76. package/pipeline/skills/shared/external/coreml/SKILL.md +323 -367
  77. package/pipeline/skills/shared/external/coreml/evals/evals.json +24 -21
  78. package/pipeline/skills/shared/external/coreml/references/coreml-swift-integration.md +562 -565
  79. package/pipeline/skills/shared/external/council/SKILL.md +1 -0
  80. package/pipeline/skills/shared/external/cryptokit/SKILL.md +253 -394
  81. package/pipeline/skills/shared/external/cryptokit/evals/evals.json +20 -18
  82. package/pipeline/skills/shared/external/cryptokit/references/cryptokit-patterns.md +299 -488
  83. package/pipeline/skills/shared/external/css-modern/SKILL.md +1 -0
  84. package/pipeline/skills/shared/external/database-patterns/SKILL.md +1 -0
  85. package/pipeline/skills/shared/external/debugging-instruments/SKILL.md +270 -323
  86. package/pipeline/skills/shared/external/debugging-instruments/evals/evals.json +27 -30
  87. package/pipeline/skills/shared/external/debugging-instruments/references/instruments-guide.md +167 -315
  88. package/pipeline/skills/shared/external/debugging-instruments/references/lldb-patterns.md +140 -193
  89. package/pipeline/skills/shared/external/device-integrity/SKILL.md +230 -353
  90. package/pipeline/skills/shared/external/device-integrity/evals/evals.json +25 -21
  91. package/pipeline/skills/shared/external/device-integrity/references/device-integrity-patterns.md +159 -197
  92. package/pipeline/skills/shared/external/energykit/SKILL.md +225 -392
  93. package/pipeline/skills/shared/external/energykit/evals/evals.json +29 -28
  94. package/pipeline/skills/shared/external/energykit/references/energykit-patterns.md +174 -470
  95. package/pipeline/skills/shared/external/eventkit-calendar/SKILL.md +261 -383
  96. package/pipeline/skills/shared/external/eventkit-calendar/evals/evals.json +25 -22
  97. package/pipeline/skills/shared/external/eventkit-calendar/references/eventkit-patterns.md +165 -268
  98. package/pipeline/skills/shared/external/evidence-github/SKILL.md +2 -0
  99. package/pipeline/skills/shared/external/evidence-registry/SKILL.md +2 -0
  100. package/pipeline/skills/shared/external/gradle-kotlin-dsl/SKILL.md +2 -0
  101. package/pipeline/skills/shared/external/healthkit/SKILL.md +252 -303
  102. package/pipeline/skills/shared/external/healthkit/evals/evals.json +24 -23
  103. package/pipeline/skills/shared/external/healthkit/references/healthkit-patterns.md +369 -523
  104. package/pipeline/skills/shared/external/homekit-matter/SKILL.md +233 -348
  105. package/pipeline/skills/shared/external/homekit-matter/evals/evals.json +27 -22
  106. package/pipeline/skills/shared/external/homekit-matter/references/matter-commissioning.md +199 -305
  107. package/pipeline/skills/shared/external/html-semantic/SKILL.md +1 -0
  108. package/pipeline/skills/shared/external/humanizer/SKILL.md +1 -0
  109. package/pipeline/skills/shared/external/ios-accessibility/SKILL.md +368 -340
  110. package/pipeline/skills/shared/external/ios-accessibility/evals/evals.json +28 -27
  111. package/pipeline/skills/shared/external/ios-accessibility/references/a11y-patterns.md +314 -260
  112. package/pipeline/skills/shared/external/ios-accessibility/references/media-accessibility.md +97 -67
  113. package/pipeline/skills/shared/external/ios-accessibility/references/nutrition-labels.md +165 -101
  114. package/pipeline/skills/shared/external/ios-coding-standard/SKILL.md +1 -0
  115. package/pipeline/skills/shared/external/ios-localization/SKILL.md +258 -371
  116. package/pipeline/skills/shared/external/ios-localization/evals/evals.json +23 -23
  117. package/pipeline/skills/shared/external/ios-localization/references/formatstyle-locale.md +283 -491
  118. package/pipeline/skills/shared/external/ios-localization/references/string-catalogs.md +313 -440
  119. package/pipeline/skills/shared/external/ios-module-structure/SKILL.md +1 -0
  120. package/pipeline/skills/shared/external/ios-networking/SKILL.md +265 -341
  121. package/pipeline/skills/shared/external/ios-networking/evals/evals.json +24 -24
  122. package/pipeline/skills/shared/external/ios-networking/references/background-websocket.md +425 -652
  123. package/pipeline/skills/shared/external/ios-networking/references/file-storage-patterns.md +143 -285
  124. package/pipeline/skills/shared/external/ios-networking/references/lightweight-clients.md +93 -53
  125. package/pipeline/skills/shared/external/ios-networking/references/network-framework.md +231 -456
  126. package/pipeline/skills/shared/external/ios-networking/references/urlsession-patterns.md +517 -784
  127. package/pipeline/skills/shared/external/ios-security/SKILL.md +2 -0
  128. package/pipeline/skills/shared/external/ios-simulator/SKILL.md +265 -393
  129. package/pipeline/skills/shared/external/ios-simulator/evals/evals.json +21 -21
  130. package/pipeline/skills/shared/external/ios-simulator/references/simctl-commands.md +177 -270
  131. package/pipeline/skills/shared/external/live-activities/SKILL.md +318 -360
  132. package/pipeline/skills/shared/external/live-activities/evals/evals.json +21 -21
  133. package/pipeline/skills/shared/external/live-activities/references/activitykit-patterns.md +478 -710
  134. package/pipeline/skills/shared/external/localization-reuse-map/SKILL.md +91 -283
  135. package/pipeline/skills/shared/external/localization-reuse-map/reference/format-and-output.md +119 -151
  136. package/pipeline/skills/shared/external/localization-reuse-map/reference/publish-and-snapshot.md +60 -90
  137. package/pipeline/skills/shared/external/localization-reuse-map/reference/sources-and-recipes.md +119 -156
  138. package/pipeline/skills/shared/external/localization-reuse-map/scripts/build-artifact.py +726 -787
  139. package/pipeline/skills/shared/external/localization-reuse-map/scripts/build-spreadsheet.py +253 -288
  140. package/pipeline/skills/shared/external/localization-reuse-map/scripts/fetch-annotations.py +243 -304
  141. package/pipeline/skills/shared/external/localization-reuse-map/scripts/fetch-legacy-labels.py +88 -104
  142. package/pipeline/skills/shared/external/localization-reuse-map/scripts/publish-confluence.py +181 -235
  143. package/pipeline/skills/shared/external/localization-reuse-map/scripts/render-key-shots.py +198 -263
  144. package/pipeline/skills/shared/external/localization-reuse-map/scripts/render-overlay.py +461 -466
  145. package/pipeline/skills/shared/external/localization-reuse-map/scripts/resolve-legacy-values.py +145 -151
  146. package/pipeline/skills/shared/external/localization-reuse-map/scripts/resolve-new-values.py +123 -141
  147. package/pipeline/skills/shared/external/localization-reuse-map/scripts/scan-screen-keys.py +146 -157
  148. package/pipeline/skills/shared/external/localization-reuse-map/scripts/snapshot-resources.sh +22 -19
  149. package/pipeline/skills/shared/external/localization-reuse-map/scripts/verify-map.py +156 -140
  150. package/pipeline/skills/shared/external/mapkit-location/SKILL.md +295 -267
  151. package/pipeline/skills/shared/external/mapkit-location/evals/evals.json +28 -24
  152. package/pipeline/skills/shared/external/mapkit-location/references/mapkit-corelocation-patterns.md +378 -532
  153. package/pipeline/skills/shared/external/mapkit-location/references/mapkit-patterns.md +397 -499
  154. package/pipeline/skills/shared/external/metrickit-diagnostics/SKILL.md +165 -348
  155. package/pipeline/skills/shared/external/metrickit-diagnostics/evals/evals.json +26 -23
  156. package/pipeline/skills/shared/external/metrickit-diagnostics/references/metrickit-patterns.md +123 -130
  157. package/pipeline/skills/shared/external/musickit-audio/SKILL.md +189 -315
  158. package/pipeline/skills/shared/external/musickit-audio/evals/evals.json +22 -21
  159. package/pipeline/skills/shared/external/musickit-audio/references/musickit-patterns.md +181 -270
  160. package/pipeline/skills/shared/external/natural-language/SKILL.md +188 -340
  161. package/pipeline/skills/shared/external/natural-language/evals/evals.json +21 -21
  162. package/pipeline/skills/shared/external/natural-language/references/translation-patterns.md +171 -225
  163. package/pipeline/skills/shared/external/nextjs-app-router/SKILL.md +1 -0
  164. package/pipeline/skills/shared/external/passkit-wallet/SKILL.md +258 -392
  165. package/pipeline/skills/shared/external/passkit-wallet/evals/evals.json +30 -29
  166. package/pipeline/skills/shared/external/passkit-wallet/references/wallet-passes.md +164 -231
  167. package/pipeline/skills/shared/external/pdfkit/SKILL.md +312 -344
  168. package/pipeline/skills/shared/external/pdfkit/evals/evals.json +19 -19
  169. package/pipeline/skills/shared/external/pdfkit/references/pdfkit-patterns.md +413 -624
  170. package/pipeline/skills/shared/external/pencilkit-drawing/SKILL.md +242 -358
  171. package/pipeline/skills/shared/external/pencilkit-drawing/evals/evals.json +25 -21
  172. package/pipeline/skills/shared/external/pencilkit-drawing/references/pencilkit-patterns.md +161 -226
  173. package/pipeline/skills/shared/external/permissionkit/SKILL.md +282 -400
  174. package/pipeline/skills/shared/external/permissionkit/evals/evals.json +27 -30
  175. package/pipeline/skills/shared/external/permissionkit/references/permissionkit-patterns.md +237 -350
  176. package/pipeline/skills/shared/external/photos-camera-media/SKILL.md +276 -325
  177. package/pipeline/skills/shared/external/photos-camera-media/references/av-playback.md +299 -545
  178. package/pipeline/skills/shared/external/photos-camera-media/references/camera-capture.md +344 -588
  179. package/pipeline/skills/shared/external/photos-camera-media/references/image-loading-caching.md +316 -660
  180. package/pipeline/skills/shared/external/photos-camera-media/references/photokit-patterns.md +270 -416
  181. package/pipeline/skills/shared/external/play-store-review/SKILL.md +2 -0
  182. package/pipeline/skills/shared/external/push-notifications/SKILL.md +312 -340
  183. package/pipeline/skills/shared/external/push-notifications/evals/evals.json +27 -26
  184. package/pipeline/skills/shared/external/push-notifications/references/notification-patterns.md +328 -485
  185. package/pipeline/skills/shared/external/push-notifications/references/rich-notifications.md +327 -560
  186. package/pipeline/skills/shared/external/python-patterns/SKILL.md +1 -0
  187. package/pipeline/skills/shared/external/react-best-practices/SKILL.md +1 -0
  188. package/pipeline/skills/shared/external/realitykit-ar/SKILL.md +218 -410
  189. package/pipeline/skills/shared/external/realitykit-ar/evals/evals.json +24 -27
  190. package/pipeline/skills/shared/external/realitykit-ar/references/realitykit-patterns.md +221 -348
  191. package/pipeline/skills/shared/external/rest-api-design/SKILL.md +1 -0
  192. package/pipeline/skills/shared/external/retrofit-networking/SKILL.md +2 -0
  193. package/pipeline/skills/shared/external/room-database/SKILL.md +2 -0
  194. package/pipeline/skills/shared/external/search-first/SKILL.md +1 -0
  195. package/pipeline/skills/shared/external/shareplay-activities/SKILL.md +222 -393
  196. package/pipeline/skills/shared/external/shareplay-activities/evals/evals.json +23 -24
  197. package/pipeline/skills/shared/external/shareplay-activities/references/shareplay-patterns.md +280 -420
  198. package/pipeline/skills/shared/external/signal-community/SKILL.md +2 -0
  199. package/pipeline/skills/shared/external/skill-creator/SKILL.md +80 -41
  200. package/pipeline/skills/shared/external/skill-creator/audit.md +63 -59
  201. package/pipeline/skills/shared/external/skill-creator/checklist.md +28 -20
  202. package/pipeline/skills/shared/external/skill-creator/examples.md +40 -40
  203. package/pipeline/skills/shared/external/skill-creator/label-check.md +48 -36
  204. package/pipeline/skills/shared/external/skill-creator/scripts/audit-panel.js +91 -100
  205. package/pipeline/skills/shared/external/skill-creator/template.md +51 -39
  206. package/pipeline/skills/shared/external/speech-recognition/SKILL.md +217 -421
  207. package/pipeline/skills/shared/external/speech-recognition/evals/evals.json +23 -26
  208. package/pipeline/skills/shared/external/speech-recognition/references/speechanalyzer-patterns.md +133 -125
  209. package/pipeline/skills/shared/external/storekit/SKILL.md +228 -204
  210. package/pipeline/skills/shared/external/storekit/evals/evals.json +27 -24
  211. package/pipeline/skills/shared/external/storekit/references/app-review-guidelines.md +98 -109
  212. package/pipeline/skills/shared/external/storekit/references/core-patterns.md +298 -242
  213. package/pipeline/skills/shared/external/storekit/references/storekit-advanced.md +356 -649
  214. package/pipeline/skills/shared/external/swift-api-design-guidelines/SKILL.md +274 -399
  215. package/pipeline/skills/shared/external/swift-api-design-guidelines/evals/evals.json +22 -24
  216. package/pipeline/skills/shared/external/swift-api-design-guidelines/references/argument-labels-and-parameters.md +107 -108
  217. package/pipeline/skills/shared/external/swift-api-design-guidelines/references/conventions-and-special-rules.md +93 -165
  218. package/pipeline/skills/shared/external/swift-api-design-guidelines/references/naming-and-clarity.md +99 -137
  219. package/pipeline/skills/shared/external/swift-api-design-guidelines/references/side-effects-and-mutating-pairs.md +77 -120
  220. package/pipeline/skills/shared/external/swift-architecture/SKILL.md +334 -350
  221. package/pipeline/skills/shared/external/swift-architecture/evals/evals.json +22 -22
  222. package/pipeline/skills/shared/external/swift-charts/SKILL.md +208 -394
  223. package/pipeline/skills/shared/external/swift-charts/evals/evals.json +27 -30
  224. package/pipeline/skills/shared/external/swift-charts/references/charts-patterns.md +351 -762
  225. package/pipeline/skills/shared/external/swift-codable/SKILL.md +339 -343
  226. package/pipeline/skills/shared/external/swift-codable/evals/evals.json +20 -20
  227. package/pipeline/skills/shared/external/swift-concurrency/SKILL.md +303 -351
  228. package/pipeline/skills/shared/external/swift-concurrency/evals/evals.json +27 -24
  229. package/pipeline/skills/shared/external/swift-concurrency/references/approachable-concurrency.md +65 -80
  230. package/pipeline/skills/shared/external/swift-concurrency/references/async-algorithms.md +48 -84
  231. package/pipeline/skills/shared/external/swift-concurrency/references/bridging-interop.md +134 -79
  232. package/pipeline/skills/shared/external/swift-concurrency/references/concurrency-patterns.md +145 -167
  233. package/pipeline/skills/shared/external/swift-concurrency/references/diagnostics.md +62 -50
  234. package/pipeline/skills/shared/external/swift-concurrency/references/swiftui-concurrency.md +92 -121
  235. package/pipeline/skills/shared/external/swift-concurrency/references/synchronization-primitives.md +177 -241
  236. package/pipeline/skills/shared/external/swift-formatstyle/SKILL.md +258 -234
  237. package/pipeline/skills/shared/external/swift-language/SKILL.md +342 -382
  238. package/pipeline/skills/shared/external/swift-language/evals/evals.json +24 -27
  239. package/pipeline/skills/shared/external/swift-language/references/swift-attributes-interop.md +79 -56
  240. package/pipeline/skills/shared/external/swift-language/references/swift-patterns-extended.md +297 -340
  241. package/pipeline/skills/shared/external/swift-security/SKILL.md +180 -161
  242. package/pipeline/skills/shared/external/swift-security/evals/evals.json +25 -25
  243. package/pipeline/skills/shared/external/swift-security/references/biometric-authentication.md +314 -469
  244. package/pipeline/skills/shared/external/swift-security/references/certificate-trust.md +408 -476
  245. package/pipeline/skills/shared/external/swift-security/references/common-anti-patterns.md +260 -530
  246. package/pipeline/skills/shared/external/swift-security/references/compliance-owasp-mapping.md +270 -477
  247. package/pipeline/skills/shared/external/swift-security/references/credential-storage-patterns.md +573 -571
  248. package/pipeline/skills/shared/external/swift-security/references/cryptokit-public-key.md +370 -441
  249. package/pipeline/skills/shared/external/swift-security/references/cryptokit-symmetric.md +332 -433
  250. package/pipeline/skills/shared/external/swift-security/references/keychain-access-control.md +346 -468
  251. package/pipeline/skills/shared/external/swift-security/references/keychain-fundamentals.md +352 -472
  252. package/pipeline/skills/shared/external/swift-security/references/keychain-item-classes.md +431 -432
  253. package/pipeline/skills/shared/external/swift-security/references/keychain-sharing.md +328 -425
  254. package/pipeline/skills/shared/external/swift-security/references/migration-legacy-stores.md +341 -579
  255. package/pipeline/skills/shared/external/swift-security/references/secure-enclave.md +396 -457
  256. package/pipeline/skills/shared/external/swift-security/references/testing-security-code.md +354 -614
  257. package/pipeline/skills/shared/external/swift-testing/SKILL.md +188 -175
  258. package/pipeline/skills/shared/external/swift-testing/evals/evals.json +26 -24
  259. package/pipeline/skills/shared/external/swift-testing/references/testing-advanced.md +80 -84
  260. package/pipeline/skills/shared/external/swift-testing/references/testing-patterns.md +317 -433
  261. package/pipeline/skills/shared/external/swiftdata/SKILL.md +392 -256
  262. package/pipeline/skills/shared/external/swiftdata/evals/evals.json +24 -24
  263. package/pipeline/skills/shared/external/swiftdata/references/core-data-coexistence.md +206 -402
  264. package/pipeline/skills/shared/external/swiftdata/references/indexing.md +59 -52
  265. package/pipeline/skills/shared/external/swiftdata/references/predicate-pitfalls.md +57 -33
  266. package/pipeline/skills/shared/external/swiftdata/references/swiftdata-advanced.md +354 -747
  267. package/pipeline/skills/shared/external/swiftdata/references/swiftdata-queries.md +300 -508
  268. package/pipeline/skills/shared/external/swiftlint/SKILL.md +175 -226
  269. package/pipeline/skills/shared/external/swiftlint/references/adoption-and-configuration.md +141 -208
  270. package/pipeline/skills/shared/external/swiftlint/references/custom-rules-and-analyze.md +100 -109
  271. package/pipeline/skills/shared/external/swiftlint/references/plugins-run-scripts-and-integrations.md +159 -179
  272. package/pipeline/skills/shared/external/swiftlint/references/rule-reference.md +383 -18
  273. package/pipeline/skills/shared/external/swiftlint/references/rules-suppressions-and-baselines.md +143 -229
  274. package/pipeline/skills/shared/external/swiftui-animation/SKILL.md +283 -366
  275. package/pipeline/skills/shared/external/swiftui-animation/references/animation-advanced.md +396 -608
  276. package/pipeline/skills/shared/external/swiftui-animation/references/core-animation-bridge.md +336 -385
  277. package/pipeline/skills/shared/external/swiftui-gestures/SKILL.md +239 -349
  278. package/pipeline/skills/shared/external/swiftui-gestures/references/gesture-patterns.md +228 -310
  279. package/pipeline/skills/shared/external/swiftui-layout-components/SKILL.md +260 -249
  280. package/pipeline/skills/shared/external/swiftui-layout-components/references/form.md +92 -74
  281. package/pipeline/skills/shared/external/swiftui-layout-components/references/grids.md +112 -177
  282. package/pipeline/skills/shared/external/swiftui-layout-components/references/list.md +61 -64
  283. package/pipeline/skills/shared/external/swiftui-layout-components/references/scrollview.md +94 -134
  284. package/pipeline/skills/shared/external/swiftui-liquid-glass/SKILL.md +193 -225
  285. package/pipeline/skills/shared/external/swiftui-liquid-glass/references/liquid-glass.md +173 -327
  286. package/pipeline/skills/shared/external/swiftui-navigation/SKILL.md +193 -168
  287. package/pipeline/skills/shared/external/swiftui-navigation/references/deeplinks.md +127 -150
  288. package/pipeline/skills/shared/external/swiftui-navigation/references/navigationstack.md +132 -133
  289. package/pipeline/skills/shared/external/swiftui-navigation/references/sheets.md +152 -117
  290. package/pipeline/skills/shared/external/swiftui-navigation/references/tabview.md +106 -140
  291. package/pipeline/skills/shared/external/swiftui-patterns/SKILL.md +316 -252
  292. package/pipeline/skills/shared/external/swiftui-patterns/references/architecture-patterns.md +341 -332
  293. package/pipeline/skills/shared/external/swiftui-patterns/references/deprecated-migration.md +547 -854
  294. package/pipeline/skills/shared/external/swiftui-patterns/references/design-polish.md +485 -537
  295. package/pipeline/skills/shared/external/swiftui-patterns/references/platform-and-sharing.md +417 -499
  296. package/pipeline/skills/shared/external/swiftui-performance/SKILL.md +213 -376
  297. package/pipeline/skills/shared/external/swiftui-performance/references/demystify-swiftui-performance-wwdc23.md +86 -175
  298. package/pipeline/skills/shared/external/swiftui-performance/references/optimizing-swiftui-performance-instruments.md +89 -195
  299. package/pipeline/skills/shared/external/swiftui-performance/references/understanding-hangs-in-your-app.md +95 -182
  300. package/pipeline/skills/shared/external/swiftui-performance/references/understanding-improving-swiftui-performance.md +71 -149
  301. package/pipeline/skills/shared/external/swiftui-performance/references/wwdc-session-sources.md +21 -27
  302. package/pipeline/skills/shared/external/swiftui-uikit-interop/SKILL.md +303 -295
  303. package/pipeline/skills/shared/external/swiftui-uikit-interop/references/hosting-migration.md +204 -387
  304. package/pipeline/skills/shared/external/swiftui-uikit-interop/references/representable-recipes.md +469 -683
  305. package/pipeline/skills/shared/external/swiftui-webkit/SKILL.md +140 -186
  306. package/pipeline/skills/shared/external/swiftui-webkit/references/loading-and-observation.md +75 -86
  307. package/pipeline/skills/shared/external/swiftui-webkit/references/local-content-and-custom-schemes.md +63 -60
  308. package/pipeline/skills/shared/external/swiftui-webkit/references/migration-and-fallbacks.md +69 -137
  309. package/pipeline/skills/shared/external/swiftui-webkit/references/navigation-and-javascript.md +95 -67
  310. package/pipeline/skills/shared/external/tailwind-css/SKILL.md +1 -0
  311. package/pipeline/skills/shared/external/testing-backend/SKILL.md +1 -0
  312. package/pipeline/skills/shared/external/tipkit/SKILL.md +220 -335
  313. package/pipeline/skills/shared/external/tipkit/references/tipkit-patterns.md +356 -494
  314. package/pipeline/skills/shared/external/typescript-patterns/SKILL.md +1 -0
  315. package/pipeline/skills/shared/external/vision-framework/SKILL.md +260 -375
  316. package/pipeline/skills/shared/external/vision-framework/references/vision-requests.md +393 -515
  317. package/pipeline/skills/shared/external/vision-framework/references/visionkit-scanner.md +363 -539
  318. package/pipeline/skills/shared/external/vue-composition/SKILL.md +1 -0
  319. package/pipeline/skills/shared/external/weatherkit/SKILL.md +152 -310
  320. package/pipeline/skills/shared/external/weatherkit/references/weatherkit-patterns.md +288 -407
  321. package/pipeline/skills/shared/external/web-accessibility/SKILL.md +1 -0
  322. package/pipeline/skills/shared/external/web-performance/SKILL.md +1 -0
  323. package/pipeline/skills/shared/external/web-testing/SKILL.md +1 -0
  324. package/pipeline/skills/shared/external/widgetkit/SKILL.md +216 -288
  325. package/pipeline/skills/shared/external/widgetkit/references/widgetkit-advanced.md +414 -719
  326. 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.