Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 9 additions & 9 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,7 @@ All notable changes to this project will be documented in this file.

## [Unreleased]

### Added

- **`MagicVaultService` takes `macOsUsesDataProtectionKeychain`, so a macOS build with no signing identity can store a secret at all.** macOS has two keychains and the service previously passed no `mOptions`, which left it on `flutter_secure_storage`'s default of the data protection one (`macos_options.dart:24`). That keychain requires the `keychain-access-groups` entitlement, the entitlement is restricted and therefore forces a signed build with an App ID, and on a build with no certificate installed **every write fails** with `PlatformException(-34018, errSecMissingEntitlement)`; measured on a consumer app's own build. Passing `false` moves the items to the legacy login keychain, which needs no entitlement. Note that `false` does not set `kSecUseDataProtectionKeychain` to false, it OMITS the key from the query entirely (`flutter_secure_storage_darwin` `FlutterSecureStorage.swift:227-231`, guarded by `params.usesDataProtectionKeychain`), which is the mechanism by which the item lands in the other keychain rather than a flag the keychain reads.

**The default is `true`, which is what every existing build already does, and it stays that way because there is no migration between the two keychains in either direction.** An item written to one is invisible from the other, and every caller reads that miss as "never stored" rather than as an error: `Crypt.encryptWithDeviceKey` generates a fresh device key on a null read (`crypt.dart:141-147`), making everything encrypted under the old one permanently unreadable while the old key sits unreachable in the other keychain, and `BaseGuard` loses the stored token the same way and logs the user out. Flip the key once, before the app has stored anything; do not reach for it to clear a `-34018` on a build that has been storing secrets, and read `doc/security/vault.md` for how to move existing items across.

`VaultServiceProvider` reads it from `security.vault.macos_data_protection_keychain`, inside the singleton factory rather than beside it, so a test or a hand-registered provider that sets the key afterwards is still read. macOS also now gets `first_unlock_this_device` accessibility, rather than the package default `unlocked` (`apple_options.dart:72`), for the same reason iOS already passes `first_unlock`: a read while the screen is locked, such as a refresh on launch before anyone has touched the machine, succeeds under the former and fails under the latter, and the `ThisDeviceOnly` half keeps the item off a backup restored onto a different Mac. `kSecAttrAccessible` is a data protection keychain attribute, so that class is inert once the key above is `false`. The iOS and Android options are unchanged. (`lib/src/security/magic_vault_service.dart`, `lib/src/security/vault_service_provider.dart`)

- **`FakeVaultService.throwOnGet`, `.throwOnPut`, `.throwOnRemove` and `.throwOnFlush`, so a consumer can test its own vault-failure branch.** Every prior override was a no-throw body over an in-memory map, so nothing exercised the path a real keychain failure takes through `MagicVaultService`, which wraps a `PlatformException` as `MagicVaultException` on all four operations. `throwOnRemove` is the one a sign-out needs: a consumer that deletes the stored credential first and clears its in-memory session afterwards has two branches through one keychain call, and the failing one decides whether the user is told the secret is still on the device or is shown a sign-out that did nothing. All four default to a `MagicVaultException` and accept a custom error, all four are cleared by `reset()`, and each only affects its own operation: a throw armed on `get` does not touch `put`, and one armed on `remove` does not touch `flush`. The throw is armed for every key, so a test needing one key to fail while its neighbours succeed still needs its own subclass; a key filter would have to record the attempt before the throw, and that would make `assertWritten` pass for a `put` that threw. (`lib/src/testing/fake_vault_service.dart`)
## [0.0.10] - 2026-09-11

### BREAKING

Expand All @@ -28,6 +20,14 @@ All notable changes to this project will be documented in this file.

### Added

- **`MagicVaultService` takes `macOsUsesDataProtectionKeychain`, so a macOS build with no signing identity can store a secret at all.** macOS has two keychains and the service previously passed no `mOptions`, which left it on `flutter_secure_storage`'s default of the data protection one (`macos_options.dart:24`). That keychain requires the `keychain-access-groups` entitlement, the entitlement is restricted and therefore forces a signed build with an App ID, and on a build with no certificate installed **every write fails** with `PlatformException(-34018, errSecMissingEntitlement)`; measured on a consumer app's own build. Passing `false` moves the items to the legacy login keychain, which needs no entitlement. Note that `false` does not set `kSecUseDataProtectionKeychain` to false, it OMITS the key from the query entirely (`flutter_secure_storage_darwin` `FlutterSecureStorage.swift:227-231`, guarded by `params.usesDataProtectionKeychain`), which is the mechanism by which the item lands in the other keychain rather than a flag the keychain reads.

**The default is `true`, which is what every existing build already does, and it stays that way because there is no migration between the two keychains in either direction.** An item written to one is invisible from the other, and every caller reads that miss as "never stored" rather than as an error: `Crypt.encryptWithDeviceKey` generates a fresh device key on a null read (`crypt.dart:141-147`), making everything encrypted under the old one permanently unreadable while the old key sits unreachable in the other keychain, and `BaseGuard` loses the stored token the same way and logs the user out. Flip the key once, before the app has stored anything; do not reach for it to clear a `-34018` on a build that has been storing secrets, and read `doc/security/vault.md` for how to move existing items across.

`VaultServiceProvider` reads it from `security.vault.macos_data_protection_keychain`, inside the singleton factory rather than beside it, so a test or a hand-registered provider that sets the key afterwards is still read. macOS also now gets `first_unlock_this_device` accessibility, rather than the package default `unlocked` (`apple_options.dart:72`), for the same reason iOS already passes `first_unlock`: a read while the screen is locked, such as a refresh on launch before anyone has touched the machine, succeeds under the former and fails under the latter, and the `ThisDeviceOnly` half keeps the item off a backup restored onto a different Mac. `kSecAttrAccessible` is a data protection keychain attribute, so that class is inert once the key above is `false`. The iOS and Android options are unchanged. (`lib/src/security/magic_vault_service.dart`, `lib/src/security/vault_service_provider.dart`)

- **`FakeVaultService.throwOnGet`, `.throwOnPut`, `.throwOnRemove` and `.throwOnFlush`, so a consumer can test its own vault-failure branch.** Every prior override was a no-throw body over an in-memory map, so nothing exercised the path a real keychain failure takes through `MagicVaultService`, which wraps a `PlatformException` as `MagicVaultException` on all four operations. `throwOnRemove` is the one a sign-out needs: a consumer that deletes the stored credential first and clears its in-memory session afterwards has two branches through one keychain call, and the failing one decides whether the user is told the secret is still on the device or is shown a sign-out that did nothing. All four default to a `MagicVaultException` and accept a custom error, all four are cleared by `reset()`, and each only affects its own operation: a throw armed on `get` does not touch `put`, and one armed on `remove` does not touch `flush`. The throw is armed for every key, so a test needing one key to fail while its neighbours succeed still needs its own subclass; a key filter would have to record the attempt before the throw, and that would make `assertWritten` pass for a `put` that threw. (`lib/src/testing/fake_vault_service.dart`)

- **`MagicSelector<C, T>` rebuilds one subtree when one part of a controller changes.** `refreshUI()` notifies every listener and `MagicStatefulViewState` answers with `setState` on the whole view, which is the right default and stops being cheap on a screen where one field changes often and most of the screen does not care: a consumer measured one keystroke in a search field rebuilding 220 styled containers. `MagicBuilder` could not help, because it needs a `ValueListenable` and a controller is a `ChangeNotifier`. The selector caches the widget its builder returned and, while the selected value compares equal, returns that same instance, so `Element.updateChild` short circuits on `child.widget == newWidget` and never descends. Returning an identical instance rather than skipping a `setState` is what makes it work under a parent that rebuilds anyway. Two rules follow: `builder` must be a pure function of the selected value (select a record to watch several fields), and equality is plain `==`, so a selector returning a freshly built `List` never matches its own cache. Deep comparison is deliberately not used, because walking a ten thousand element list per keystroke costs more than the rebuild it prevents. (`lib/src/ui/magic_selector.dart`)

- **`MagicPaginator.isRefreshing` and `.isLoadingMore`, because a list has three loading states and one flag cannot carry them.** A first load shows a skeleton, a refresh keeps the rows the reader is already looking at, and a next page puts a footer under the last row. Read off `isLoading` alone the second and third are indistinguishable, so a screen either blanks itself on every filter change or grows a footer promising a page nothing asked for. Both are false on a first load (nothing on screen to preserve, nothing being appended) and all three are false once the request lands. The distinction only exists DURING a request, which is why `_isReset` is set beside `_isLoading` and before the notification rather than derived afterwards: by the time a caller can await the future there is nothing left to tell apart. One window is documented rather than changed: a `refresh()` deferred behind an in-flight `loadMore()` keeps reporting `isLoadingMore` until that page lands, which is what is happening on the wire and the only path where the flags follow the request rather than the caller's most recent ask. (`lib/src/http/magic_paginator.dart`)
Expand Down
2 changes: 1 addition & 1 deletion example/pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -595,7 +595,7 @@ packages:
path: ".."
relative: true
source: path
version: "0.0.9"
version: "0.0.10"
matcher:
dependency: transitive
description:
Expand Down
2 changes: 1 addition & 1 deletion example/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ dependencies:
cupertino_icons: ^1.0.9
magic:
path: ..
version: 0.0.9
version: 0.0.10
fluttersdk_artisan: ^0.0.14

dev_dependencies:
Expand Down
2 changes: 1 addition & 1 deletion pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name: magic
description: "A Laravel-inspired Flutter framework with Eloquent ORM, routing, and MVC architecture."
version: 0.0.9
version: 0.0.10
homepage: https://magic.fluttersdk.com
repository: https://github.com/fluttersdk/magic
issue_tracker: https://github.com/fluttersdk/magic/issues
Expand Down
2 changes: 1 addition & 1 deletion skills/magic-framework/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ when_to_use: "Use proactively when editing or scaffolding a magic app: Magic.ini
version: 0.1.16
---

<!-- magic 0.0.9 | Skill v0.1.16 (2026-09-10). API surface verified against lib/src. -->
<!-- magic 0.0.10 | Skill v0.1.16 (2026-09-10). API surface verified against lib/src. -->

# Magic Framework

Expand Down
Loading