macos_security_scoped_bookmarks 0.1.0
macos_security_scoped_bookmarks: ^0.1.0 copied to clipboard
Token-based macOS security-scoped bookmark creation, resolution, and access lifetimes.
macOS Security-Scoped Bookmarks #
macos_security_scoped_bookmarks is a Flutter plugin for creating, refreshing,
and safely accessing app-scoped macOS security bookmarks.
The package owns every native resolution and exposes it only as a callback-
scoped resource. Access, refresh, and cleanup therefore use the exact same
native NSURL, even when separate bookmarks resolve to the same filesystem
path. File and directory kinds are detected by macOS rather than supplied by
the caller.
How does this compare with macos_secure_bookmarks? #
This package is a modern, safety-focused alternative to
macos_secure_bookmarks.
The comparison below targets version 0.2.1, the latest published version at the
time of writing.
macos_security_scoped_bookmarks |
macos_secure_bookmarks 0.2.1 |
|
|---|---|---|
| Access lifetime | Automatically bounded by withBookmarkAccess |
Callers must manually resolve, start, and stop access |
| Native resource identity | A unique token retains each exact resolved NSURL |
Resolved URLs are indexed by filesystem path |
| Concurrent same-path resolutions | Kept independent | Can overwrite one another in the native path map |
| Permissions | Read-only by default; read/write is explicit | Creates bookmarks without the read-only restriction |
| Stale bookmarks | Reports staleness and can refresh the exact resolved URL | Detects staleness internally but cannot expose or refresh it |
| File or directory | Detected by macOS | Supplied by the caller through isDirectory |
| Cleanup failures | Automatic cleanup; preserves operation and cleanup errors | Cleanup and error preservation are the caller's responsibility |
| Persisted value | Versioned value containing bookmark data and its access mode | Raw base64 native bookmark data |
| Privacy | Does not log filesystem paths | Native implementation prints resolved paths |
| Toolchain | Dart 3.7+, Flutter 3.29+, macOS 10.14+ | Dart 2.12–2.x, Flutter 1.17+, macOS 10.11+ |
| Native integration | CocoaPods and Swift Package Manager | CocoaPods |
For new applications, we recommend macos_security_scoped_bookmarks. Its
callback API makes the safe lifecycle the default: protected work finishes
before access is released, cleanup also runs after exceptions, and concurrent
bookmarks do not collide merely because they resolve to the same path. It also
supports least-privilege read-only bookmarks and the stale-bookmark refresh
flow required by long-lived applications.
macos_secure_bookmarks may still be relevant when an application must support
macOS 10.11–10.13 or an older pre-Dart-3 Flutter toolchain. Its manual API may
also be easier to adopt temporarily in an existing application already built
around it, but the application must balance every successful start with a stop
and handle stale bookmarks itself.
The two packages are not drop-in API replacements. In particular, an existing
macos_secure_bookmarks value is raw native bookmark data, while this package
persists a versioned value that also records whether access is read-only or
read/write. Plan an explicit data migration and choose the appropriate access
mode before switching an application with stored bookmarks.
Usage #
Create bookmark data immediately after an authorized user action, such as an operating-system file picker or drag and drop. Bookmarks are read-only by default; request read/write access explicitly only when it is needed:
import 'dart:io';
import 'package:macos_security_scoped_bookmarks/macos_security_scoped_bookmarks.dart';
final bookmark = await createBookmark(
file,
access: MacOSSecurityScopedBookmarkAccess.readOnly,
);
// Persist this complete opaque value, not native bookmark data by itself.
await storage.write(bookmark.encode());
Restore the complete bookmark value, then perform filesystem work inside
withBookmarkAccess. The callback receives the resolved file or directory only
while its native security scope is active:
final bookmark = MacOSSecurityScopedBookmark.decode(await storage.read());
final byteCount = await withBookmarkAccess(
bookmark,
(resource) async {
if (resource.isStale) {
final refreshedBookmark = await resource.refreshBookmark();
// Atomically replace the stored bookmark.
await storage.write(refreshedBookmark.encode());
}
return (resource.entity as File).length();
},
);
Do not retain the resource or its entity after the callback completes.
withBookmarkAccess always attempts to stop native access and discard the
resolution. It also preserves both errors if the operation and cleanup fail.
The encoded MacOSSecurityScopedBookmark keeps native bookmark data and its
access mode together, and refresh automatically preserves that mode.
All protected filesystem work must finish inside the callback. Return only
detached results such as bytes, strings, paths, counts, or application models.
A future-returning operation such as readAsBytes is safe because
withBookmarkAccess waits for that future:
final bytes = await withBookmarkAccess(
bookmark,
(resource) => (resource.entity as File).readAsBytes(),
);
Do not return the resource, its entity, a stream, or an open filesystem handle. Creating a stream does not consume it, so this releases access too early:
// Incorrect: the security scope ends before this stream is consumed.
final stream = await withBookmarkAccess(
bookmark,
(resource) => (resource.entity as File).openRead(),
);
Consume lazy operations completely inside the callback instead:
final chunks = await withBookmarkAccess(
bookmark,
(resource) => (resource.entity as File).openRead().toList(),
);
If starting access fails at the channel boundary or receives a malformed
response, the plugin cannot know whether native access started. It therefore
attempts to stop access before rethrowing the original error. If cleanup also
fails, MacOSSecurityScopedBookmarkCleanupException retains both failures.
Isolates #
The default service is stateless and can be used from multiple isolates. Before
calling plugins in a spawned background isolate, initialize its
BackgroundIsolateBinaryMessenger with the root isolate token. Transfer an
encoded MacOSSecurityScopedBookmark between isolates, decode it in the target
isolate, and complete withBookmarkAccess before that isolate exits. Never
transfer or retain the callback-scoped resource.
macOS entitlements #
Sandboxed applications need app-scoped bookmark access:
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.files.bookmarks.app-scope</key>
<true/>
For read-only bookmarks and picker access, add:
<key>com.apple.security.files.user-selected.read-only</key>
<true/>
For read/write bookmarks, use this entitlement instead:
<key>com.apple.security.files.user-selected.read-write</key>
<true/>
The example persists the complete bookmark value, restores it across launches, and contains complete Debug/Profile and Release entitlement files. Bookmark data reveals access to a user-selected location; choose storage appropriate for your application's privacy and threat model.
Errors and privacy #
The plugin does not log filesystem paths. Native creation, resolution, and
refresh errors are returned as PlatformExceptions whose details contain the
native error domain and code when available. withBookmarkAccess throws
MacOSSecurityScopedBookmarkAccessException when macOS refuses to activate the
security scope. If both an operation and automatic cleanup fail, it throws
MacOSSecurityScopedBookmarkCleanupException, which retains both errors and
their stack traces so callers can diagnose the operation.