Skip to content

Commit 7bdd039

Browse files
committed
feat: add External Storage Device permission support
Add support for checking and requesting external storage device authorization via AVExternalStorageDevice, available since macOS 14.0. New API: - getAuthStatus('external-storage') - check current authorization - askForExternalStorageAccess() - request authorization
1 parent ece7b6a commit 7bdd039

5 files changed

Lines changed: 86 additions & 1 deletion

File tree

README.md

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
- [`permissions.askForCalendarAccess([accessLevel])`](#permissionsaskforcalendaraccessaccesslevel)
1515
- [`permissions.askForSpeechRecognitionAccess()`](#permissionsaskforspeechrecognitionaccess)
1616
- [`permissions.askForRemindersAccess()`](#permissionsaskforremindersaccess)
17+
- [`permissions.askForExternalStorageAccess()`](#permissionsaskforexternalstorageaccess)
1718
- [`permissions.askForFocusStatusAccess()`](#permissionsaskforfocusstatusaccess)
1819
- [`permissions.askForFoldersAccess(folder)`](#permissionsaskforfoldersaccessfolder)
1920
- [`permissions.askForFullDiskAccess()`](#permissionsaskforfulldiskaccess)
@@ -39,6 +40,7 @@ This native Node.js module allows you to manage an app's access to:
3940
- Calendar
4041
- Camera
4142
- Contacts
43+
- External Storage Devices
4244
- Focus Status
4345
- Full Disk Access
4446
- Input Monitoring
@@ -62,7 +64,7 @@ If you're using macOS 12.3 or newer, you'll need to ensure you have Python insta
6264

6365
### `permissions.getAuthStatus(type)`
6466

65-
- `type` String - The type of system component to which you are requesting access. Can be one of `accessibility`, `bluetooth`, `calendar`, `camera`, `contacts`, `focus-status`, `full-disk-access`, `input-monitoring`, `location`, `microphone`, `notifications`, `photos`, `reminders`, `screen`, or `speech-recognition`.
67+
- `type` String - The type of system component to which you are requesting access. Can be one of `accessibility`, `bluetooth`, `calendar`, `camera`, `contacts`, `external-storage`, `focus-status`, `full-disk-access`, `input-monitoring`, `location`, `microphone`, `notifications`, `photos`, `reminders`, `screen`, or `speech-recognition`.
6668

6769
Returns `String` - Can be one of `not determined`, `denied`, `authorized`, `limited`, `provisional`, or `restricted`.
6870

@@ -86,6 +88,7 @@ const types = [
8688
'calendar',
8789
'camera',
8890
'contacts',
91+
'external-storage',
8992
'focus-status',
9093
'full-disk-access',
9194
'input-monitoring',
@@ -240,6 +243,27 @@ requesting Reminders access.
240243
<string>Your reason for wanting access to read and write Reminders data.</string>
241244
```
242245

246+
### `permissions.askForExternalStorageAccess()`
247+
248+
Returns `Promise<String>` - Whether or not the request succeeded or failed; can be `authorized`, `denied`, or `not determined`.
249+
250+
Checks the authorization status for capturing onto an external storage device. If the status check returns:
251+
252+
- `not determined` - The external storage access authorization will prompt the user to authorize or deny. The Promise is resolved after the user selection with either `authorized` or `denied`.
253+
- `denied` - The Promise is resolved as `denied`.
254+
255+
**Note:** Requires macOS 14.0 or higher. On older versions, the Promise resolves as `not determined`.
256+
257+
Example:
258+
259+
```js
260+
const { askForExternalStorageAccess } = require('node-mac-permissions')
261+
262+
askForExternalStorageAccess().then(status => {
263+
console.log(`Access to External Storage is ${status}`)
264+
})
265+
```
266+
243267
### `permissions.askForFocusStatusAccess()`
244268

245269
Returns `Promise<String>` - Whether or not the request succeeded or failed; can be `authorized`, `denied`, `restricted`, or `not determined`.

index.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ export function askForAccessibilityAccess(): undefined
66
export function askForCalendarAccess(accessType?: 'write-only' | 'full'): Promise<Omit<PermissionType, 'restricted'>>
77
export function askForCameraAccess(): Promise<PermissionType>
88
export function askForContactsAccess(): Promise<Omit<PermissionType, 'restricted'>>
9+
export function askForExternalStorageAccess(): Promise<PermissionType>
910
export function askForFocusStatusAccess(): Promise<PermissionType>
1011
export function askForFoldersAccess(): Promise<Omit<PermissionType, 'restricted'>>
1112
export function askForFullDiskAccess(): undefined
@@ -24,6 +25,7 @@ export type AuthType =
2425
| 'calendar'
2526
| 'camera'
2627
| 'contacts'
28+
| 'external-storage'
2729
| 'focus-status'
2830
| 'full-disk-access'
2931
| 'input-monitoring'

index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ function getAuthStatus(type) {
88
'calendar',
99
'camera',
1010
'contacts',
11+
'external-storage',
1112
'focus-status',
1213
'full-disk-access',
1314
'input-monitoring',
@@ -98,6 +99,7 @@ module.exports = {
9899
askForLocationAccess: askForLocationAccess,
99100
askForCameraAccess: permissions.askForCameraAccess,
100101
askForContactsAccess: permissions.askForContactsAccess,
102+
askForExternalStorageAccess: permissions.askForExternalStorageAccess,
101103
askForFocusStatusAccess: permissions.askForFocusStatusAccess,
102104
askForFoldersAccess,
103105
askForFullDiskAccess: permissions.askForFullDiskAccess,

permissions.mm

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -422,6 +422,25 @@ bool HasOpenSystemPreferencesDialog() {
422422
return kNotDetermined;
423423
}
424424

425+
// Returns a status indicating whether the user has authorized External Storage
426+
// Device access.
427+
std::string ExternalStorageAuthStatus() {
428+
if (@available(macOS 14.0, *)) {
429+
switch ([AVExternalStorageDevice authorizationStatus]) {
430+
case AVAuthorizationStatusAuthorized:
431+
return kAuthorized;
432+
case AVAuthorizationStatusDenied:
433+
return kDenied;
434+
case AVAuthorizationStatusRestricted:
435+
return kRestricted;
436+
default:
437+
return kNotDetermined;
438+
}
439+
}
440+
441+
return kNotDetermined;
442+
}
443+
425444
/***** EXPORTED FUNCTIONS *****/
426445

427446
// Returns the user's access consent status as a string.
@@ -464,6 +483,8 @@ bool HasOpenSystemPreferencesDialog() {
464483
auth_status = NotificationAuthStatus();
465484
} else if (type == "focus-status") {
466485
auth_status = FocusStatusAuthStatus();
486+
} else if (type == "external-storage") {
487+
auth_status = ExternalStorageAuthStatus();
467488
}
468489

469490
return Napi::Value::From(env, auth_status);
@@ -837,6 +858,39 @@ void AskForAccessibilityAccess(const Napi::CallbackInfo &info) {
837858
}
838859
}
839860

861+
// Request External Storage Device access.
862+
Napi::Promise AskForExternalStorageAccess(const Napi::CallbackInfo &info) {
863+
Napi::Env env = info.Env();
864+
Napi::Promise::Deferred deferred = Napi::Promise::Deferred::New(env);
865+
866+
if (@available(macOS 14.0, *)) {
867+
Napi::ThreadSafeFunction tsfn = Napi::ThreadSafeFunction::New(
868+
env, Napi::Function::New(env, NoOp), "externalStorageCallback", 0, 1);
869+
870+
auto callback = [=](Napi::Env env, Napi::Function js_cb,
871+
const char *status) {
872+
deferred.Resolve(Napi::String::New(env, status));
873+
};
874+
875+
std::string auth_status = ExternalStorageAuthStatus();
876+
877+
if (auth_status == kNotDetermined) {
878+
[AVExternalStorageDevice
879+
requestAccessWithCompletionHandler:^(BOOL granted) {
880+
tsfn.BlockingCall(granted ? "authorized" : "denied", callback);
881+
tsfn.Release();
882+
}];
883+
} else {
884+
tsfn.Release();
885+
deferred.Resolve(Napi::String::New(env, auth_status));
886+
}
887+
} else {
888+
deferred.Resolve(Napi::String::New(env, kNotDetermined));
889+
}
890+
891+
return deferred.Promise();
892+
}
893+
840894
// Request Focus Status access.
841895
Napi::Promise AskForFocusStatusAccess(const Napi::CallbackInfo &info) {
842896
Napi::Env env = info.Env();
@@ -898,6 +952,8 @@ void AskForAccessibilityAccess(const Napi::CallbackInfo &info) {
898952
Napi::Function::New(env, AskForCameraAccess));
899953
exports.Set(Napi::String::New(env, "askForContactsAccess"),
900954
Napi::Function::New(env, AskForContactsAccess));
955+
exports.Set(Napi::String::New(env, "askForExternalStorageAccess"),
956+
Napi::Function::New(env, AskForExternalStorageAccess));
901957
exports.Set(Napi::String::New(env, "askForFocusStatusAccess"),
902958
Napi::Function::New(env, AskForFocusStatusAccess));
903959
exports.Set(Napi::String::New(env, "askForFoldersAccess"),

test/module.spec.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ describe('node-mac-permissions', () => {
2424
'calendar',
2525
'camera',
2626
'contacts',
27+
'external-storage',
2728
'focus-status',
2829
'full-disk-access',
2930
'input-monitoring',

0 commit comments

Comments
 (0)