react-native-intune 0.1.0-alpha.1
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.
- package/LICENSE +27 -0
- package/NOTICE +49 -0
- package/README.md +164 -0
- package/RNIntune.podspec +69 -0
- package/android/build.gradle +152 -0
- package/android/consumer-rules.pro +28 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/java/com/reactnativeintune/RNIntuneAuth.kt +330 -0
- package/android/src/main/java/com/reactnativeintune/RNIntuneAuthCallback.kt +195 -0
- package/android/src/main/java/com/reactnativeintune/RNIntuneConfig.kt +63 -0
- package/android/src/main/java/com/reactnativeintune/RNIntuneEventQueue.kt +50 -0
- package/android/src/main/java/com/reactnativeintune/RNIntuneModule.kt +752 -0
- package/android/src/main/java/com/reactnativeintune/RNIntuneMsalConfig.kt +78 -0
- package/android/src/main/java/com/reactnativeintune/RNIntuneNotifications.kt +187 -0
- package/android/src/main/java/com/reactnativeintune/RNIntunePackage.kt +33 -0
- package/android/src/main/java/com/reactnativeintune/RNIntunePolicy.kt +108 -0
- package/android/src/main/java/com/reactnativeintune/RNIntuneReset.kt +85 -0
- package/android/src/main/java/com/reactnativeintune/RNIntuneResetJournal.kt +140 -0
- package/app.plugin.js +10 -0
- package/ios/RNIntune.h +20 -0
- package/ios/RNIntune.mm +467 -0
- package/ios/RNIntuneAuth.h +121 -0
- package/ios/RNIntuneAuth.m +390 -0
- package/ios/RNIntuneCore.h +150 -0
- package/ios/RNIntuneCore.m +535 -0
- package/ios/RNIntuneDelegates.h +70 -0
- package/ios/RNIntuneDelegates.m +268 -0
- package/ios/RNIntunePendingRequests.h +57 -0
- package/ios/RNIntunePendingRequests.m +230 -0
- package/ios/RNIntunePlistGuard.h +37 -0
- package/ios/RNIntunePlistGuard.m +147 -0
- package/ios/RNIntunePolicy.h +25 -0
- package/ios/RNIntunePolicy.m +112 -0
- package/ios/RNIntuneReset.h +34 -0
- package/ios/RNIntuneReset.m +93 -0
- package/ios/RNIntuneResetJournal.h +69 -0
- package/ios/RNIntuneResetJournal.m +106 -0
- package/lib/module/NativeIntune.js +20 -0
- package/lib/module/NativeIntune.js.map +1 -0
- package/lib/module/api/auth.js +66 -0
- package/lib/module/api/auth.js.map +1 -0
- package/lib/module/api/enrollment.js +104 -0
- package/lib/module/api/enrollment.js.map +1 -0
- package/lib/module/api/events.js +50 -0
- package/lib/module/api/events.js.map +1 -0
- package/lib/module/api/lifecycle.js +73 -0
- package/lib/module/api/lifecycle.js.map +1 -0
- package/lib/module/api/policy.js +23 -0
- package/lib/module/api/policy.js.map +1 -0
- package/lib/module/api/tokenProvider.js +80 -0
- package/lib/module/api/tokenProvider.js.map +1 -0
- package/lib/module/index.js +82 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/internal/decode.js +153 -0
- package/lib/module/internal/decode.js.map +1 -0
- package/lib/module/internal/emitter.js +15 -0
- package/lib/module/internal/emitter.js.map +1 -0
- package/lib/module/internal/errors.js +32 -0
- package/lib/module/internal/errors.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/types.js +143 -0
- package/lib/module/types.js.map +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/NativeIntune.d.ts +120 -0
- package/lib/typescript/src/NativeIntune.d.ts.map +1 -0
- package/lib/typescript/src/api/auth.d.ts +37 -0
- package/lib/typescript/src/api/auth.d.ts.map +1 -0
- package/lib/typescript/src/api/enrollment.d.ts +54 -0
- package/lib/typescript/src/api/enrollment.d.ts.map +1 -0
- package/lib/typescript/src/api/events.d.ts +24 -0
- package/lib/typescript/src/api/events.d.ts.map +1 -0
- package/lib/typescript/src/api/lifecycle.d.ts +33 -0
- package/lib/typescript/src/api/lifecycle.d.ts.map +1 -0
- package/lib/typescript/src/api/policy.d.ts +14 -0
- package/lib/typescript/src/api/policy.d.ts.map +1 -0
- package/lib/typescript/src/api/tokenProvider.d.ts +18 -0
- package/lib/typescript/src/api/tokenProvider.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +67 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/internal/decode.d.ts +48 -0
- package/lib/typescript/src/internal/decode.d.ts.map +1 -0
- package/lib/typescript/src/internal/emitter.d.ts +11 -0
- package/lib/typescript/src/internal/emitter.d.ts.map +1 -0
- package/lib/typescript/src/internal/errors.d.ts +22 -0
- package/lib/typescript/src/internal/errors.d.ts.map +1 -0
- package/lib/typescript/src/types.d.ts +317 -0
- package/lib/typescript/src/types.d.ts.map +1 -0
- package/package.json +204 -0
- package/plugin/transforms.js +153 -0
- package/plugin/withIntune.js +320 -0
- package/scripts/cli.mjs +72 -0
- package/scripts/doctor.mjs +232 -0
- package/scripts/fetch-sdks.mjs +456 -0
- package/scripts/lib/checks.mjs +720 -0
- package/scripts/lib/project.mjs +196 -0
- package/scripts/setup.mjs +233 -0
- package/sdk-lock.json +27 -0
- package/sdk-versions.json +70 -0
- package/src/NativeIntune.ts +130 -0
- package/src/api/auth.ts +73 -0
- package/src/api/enrollment.ts +112 -0
- package/src/api/events.ts +87 -0
- package/src/api/lifecycle.ts +75 -0
- package/src/api/policy.ts +22 -0
- package/src/api/tokenProvider.ts +80 -0
- package/src/index.ts +127 -0
- package/src/internal/decode.ts +191 -0
- package/src/internal/emitter.ts +14 -0
- package/src/internal/errors.ts +43 -0
- package/src/types.ts +381 -0
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright (c) 2026 react-native-intune contributors. MIT.
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
#import "RNIntunePlistGuard.h"
|
|
6
|
+
#import "RNIntuneCore.h"
|
|
7
|
+
|
|
8
|
+
/// The `IntuneMAMSettings` dictionary in the host app's Info.plist, and the keys inside
|
|
9
|
+
/// it that the SDK reads at launch. Verified against the strings in the 21.8.0 binary.
|
|
10
|
+
static NSString *const kSettingsKey = @"IntuneMAMSettings";
|
|
11
|
+
static NSString *const kClientId = @"ADALClientId";
|
|
12
|
+
static NSString *const kAuthority = @"ADALAuthority";
|
|
13
|
+
static NSString *const kRedirectUri = @"ADALRedirectUri";
|
|
14
|
+
static NSString *const kKeychainGroup = @"ADALCacheKeychainGroupOverride";
|
|
15
|
+
static NSString *const kMaxFileProtection = @"MaxFileProtectionLevel";
|
|
16
|
+
|
|
17
|
+
@implementation RNIntunePlistGuard
|
|
18
|
+
|
|
19
|
+
+ (NSDictionary *)settings
|
|
20
|
+
{
|
|
21
|
+
id settings = NSBundle.mainBundle.infoDictionary[kSettingsKey];
|
|
22
|
+
return [settings isKindOfClass:NSDictionary.class] ? settings : @{};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
+ (BOOL)hasIdentityKeys
|
|
26
|
+
{
|
|
27
|
+
NSDictionary *plist = [self settings];
|
|
28
|
+
return plist[kClientId] != nil || plist[kAuthority] != nil ||
|
|
29
|
+
plist[kRedirectUri] != nil;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
+ (NSString *)maxFileProtectionLevel
|
|
33
|
+
{
|
|
34
|
+
return [self settings][kMaxFileProtection] ?: @"";
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
+ (NSString *)keychainGroupOverride
|
|
38
|
+
{
|
|
39
|
+
NSString *value = [self settings][kKeychainGroup];
|
|
40
|
+
return [value isKindOfClass:NSString.class] ? value : @"";
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
+ (BOOL)check:(RNIntuneConfig *)config error:(NSError *_Nullable *_Nullable)error
|
|
44
|
+
{
|
|
45
|
+
return [self checkIdentityKeys:error] && [self checkKeychainGroup:config error:error] &&
|
|
46
|
+
[self checkFileProtection:config error:error];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
#pragma mark - 1. Hardcoded identity next to a runtime override
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* GitHub issue #405: the plist value and the runtime override disagree, and enrollment
|
|
53
|
+
* fails with no indication which one won. CLAUDE.md rule 4 forbids these keys outright
|
|
54
|
+
* for exactly this reason.
|
|
55
|
+
*/
|
|
56
|
+
+ (BOOL)checkIdentityKeys:(NSError *_Nullable *_Nullable)error
|
|
57
|
+
{
|
|
58
|
+
NSDictionary *plist = [self settings];
|
|
59
|
+
NSMutableArray<NSString *> *present = [NSMutableArray new];
|
|
60
|
+
for (NSString *key in @[ kClientId, kAuthority, kRedirectUri ]) {
|
|
61
|
+
if (plist[key] != nil) {
|
|
62
|
+
[present addObject:key];
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (present.count == 0) {
|
|
66
|
+
return YES;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (error) {
|
|
70
|
+
*error = [RNIntuneCore
|
|
71
|
+
errorWithCode:RNIntuneErrorPlistConflict
|
|
72
|
+
message:[NSString
|
|
73
|
+
stringWithFormat:
|
|
74
|
+
@"Info.plist -> IntuneMAMSettings contains %@, which "
|
|
75
|
+
@"conflicts with the values passed to configure(). This "
|
|
76
|
+
@"module configures identity at runtime only. Remove these "
|
|
77
|
+
@"keys — a conflicting plist key alongside a runtime "
|
|
78
|
+
@"override is a known cause of enrollment failing with "
|
|
79
|
+
@"AuthRequired.",
|
|
80
|
+
[present componentsJoinedByString:@", "]]];
|
|
81
|
+
}
|
|
82
|
+
return NO;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
#pragma mark - 2. Keychain group, which has no runtime setter
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* `ADALCacheKeychainGroupOverride` is read from the plist at launch, so the module cannot
|
|
89
|
+
* "set it to match" as SPEC §5.1.2 originally described. The best it can do is refuse to
|
|
90
|
+
* let the two disagree — MSAL's cache and the SDK's must never diverge.
|
|
91
|
+
*/
|
|
92
|
+
+ (BOOL)checkKeychainGroup:(RNIntuneConfig *)config
|
|
93
|
+
error:(NSError *_Nullable *_Nullable)error
|
|
94
|
+
{
|
|
95
|
+
NSString *wanted =
|
|
96
|
+
config.keychainGroupOverride.length > 0 ? config.keychainGroupOverride : nil;
|
|
97
|
+
if (wanted == nil) {
|
|
98
|
+
return YES;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
NSString *inPlist = [self settings][kKeychainGroup];
|
|
102
|
+
if ([inPlist isEqualToString:wanted]) {
|
|
103
|
+
return YES;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (error) {
|
|
107
|
+
*error = [RNIntuneCore
|
|
108
|
+
errorWithCode:RNIntuneErrorPlistConflict
|
|
109
|
+
message:[NSString stringWithFormat:
|
|
110
|
+
@"configure() asked for keychain group \"%@\" but "
|
|
111
|
+
@"Info.plist -> IntuneMAMSettings -> %@ is \"%@\". "
|
|
112
|
+
@"This key has no runtime equivalent, so it must be "
|
|
113
|
+
@"set in the plist.",
|
|
114
|
+
wanted, kKeychainGroup, inPlist ?: @"(absent)"]];
|
|
115
|
+
}
|
|
116
|
+
return NO;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
#pragma mark - 3. MaxFileProtectionLevel, likewise plist-only
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Silently ignoring a requested value is how an app ends up with a local database that
|
|
123
|
+
* becomes unreadable ten seconds after the device locks (SPEC §5.2, open question O-D).
|
|
124
|
+
*/
|
|
125
|
+
+ (BOOL)checkFileProtection:(RNIntuneConfig *)config
|
|
126
|
+
error:(NSError *_Nullable *_Nullable)error
|
|
127
|
+
{
|
|
128
|
+
BOOL isDefault = [config.maxFileProtectionLevel isEqualToString:@"complete"];
|
|
129
|
+
if (isDefault || [self settings][kMaxFileProtection] != nil) {
|
|
130
|
+
return YES;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (error) {
|
|
134
|
+
*error = [RNIntuneCore
|
|
135
|
+
errorWithCode:RNIntuneErrorPlistConflict
|
|
136
|
+
message:[NSString
|
|
137
|
+
stringWithFormat:
|
|
138
|
+
@"configure() asked for maxFileProtectionLevel \"%@\", but "
|
|
139
|
+
@"%@ has no runtime equivalent and is absent from "
|
|
140
|
+
@"Info.plist -> IntuneMAMSettings. Set it there (the SDK "
|
|
141
|
+
@"reads it at launch) or drop the option.",
|
|
142
|
+
config.maxFileProtectionLevel, kMaxFileProtection]];
|
|
143
|
+
}
|
|
144
|
+
return NO;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
@end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
//
|
|
2
|
+
// PolicySnapshot (SPEC §4.3).
|
|
3
|
+
//
|
|
4
|
+
// For adapting the host app's own UI — nothing here is enforcement. PIN prompts,
|
|
5
|
+
// screenshot blocking and encryption all happen inside the SDK and are not driven by
|
|
6
|
+
// these values.
|
|
7
|
+
//
|
|
8
|
+
// There is no clipboard field and there cannot be one: neither platform exposes a
|
|
9
|
+
// clipboard policy getter. `canSaveToPersonal` and `canOpenFromUnmanaged` serve the same
|
|
10
|
+
// purpose using values the SDK will actually answer.
|
|
11
|
+
//
|
|
12
|
+
|
|
13
|
+
#import <Foundation/Foundation.h>
|
|
14
|
+
|
|
15
|
+
NS_ASSUME_NONNULL_BEGIN
|
|
16
|
+
|
|
17
|
+
@interface RNIntunePolicy : NSObject
|
|
18
|
+
|
|
19
|
+
/// Nil only when the SDK is not linked. An unmanaged app reports permissive values
|
|
20
|
+
/// rather than `false` — reporting `false` would hide controls nothing is restricting.
|
|
21
|
+
+ (nullable NSDictionary<NSString *, id> *)snapshot;
|
|
22
|
+
|
|
23
|
+
@end
|
|
24
|
+
|
|
25
|
+
NS_ASSUME_NONNULL_END
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright (c) 2026 react-native-intune contributors. MIT.
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
#import "RNIntunePolicy.h"
|
|
6
|
+
#import "RNIntuneCore.h"
|
|
7
|
+
|
|
8
|
+
#import <IntuneMAMSwift/IntuneMAMSwift.h>
|
|
9
|
+
|
|
10
|
+
/// Names the policy channel. An SDK update that adds a source must not produce a crash
|
|
11
|
+
/// or an empty string, so anything unrecognised keeps its number.
|
|
12
|
+
static NSString *RNIntunePolicySourceName(IntuneMAMPolicySource source)
|
|
13
|
+
{
|
|
14
|
+
switch (source) {
|
|
15
|
+
case IntuneMAMPolicySource_MDM:
|
|
16
|
+
return @"MDM";
|
|
17
|
+
case IntuneMAMPolicySource_MAM:
|
|
18
|
+
return @"MAM";
|
|
19
|
+
case IntuneMAMPolicySource_Other:
|
|
20
|
+
return @"Other";
|
|
21
|
+
}
|
|
22
|
+
return [NSString stringWithFormat:@"Unknown(%ld)", (long)source];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
@implementation RNIntunePolicy
|
|
26
|
+
|
|
27
|
+
+ (nullable NSDictionary<NSString *, id> *)snapshot
|
|
28
|
+
{
|
|
29
|
+
if (!RNIntuneCore.shared.sdkAvailable) {
|
|
30
|
+
return nil;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
IntuneMAMPolicyManager *manager = IntuneMAMPolicyManager.instance;
|
|
34
|
+
|
|
35
|
+
// Ask for the *primary account's* policy, not `manager.policy`.
|
|
36
|
+
//
|
|
37
|
+
// `manager.policy` returns the policy for the current thread's identity, and no
|
|
38
|
+
// identity is set on the React Native JS thread — so the SDK answers with a default
|
|
39
|
+
// permissive policy object. It is not nil, which is what makes this so quiet: every
|
|
40
|
+
// field reads as "allowed" and the snapshot looks like a tenant with no restrictions.
|
|
41
|
+
// Found on device: an App Protection Policy with `Screen capture: Block` and Edge
|
|
42
|
+
// required still reported screenshots allowed and no managed browser.
|
|
43
|
+
//
|
|
44
|
+
// `primaryAccountId` is the single-identity accessor — the header says it is for
|
|
45
|
+
// applications that do not support multiple managed accounts, which is us by decision
|
|
46
|
+
// (SPEC §9). This is a read; it is not `setCurrentThreadAccountId`, and CLAUDE.md
|
|
47
|
+
// rule 10 still holds.
|
|
48
|
+
NSString *primaryAccountId = manager.primaryAccountId;
|
|
49
|
+
BOOL haveIdentity = primaryAccountId.length > 0;
|
|
50
|
+
|
|
51
|
+
id<IntuneMAMPolicy> policy = haveIdentity
|
|
52
|
+
? [manager policyForAccountId:primaryAccountId]
|
|
53
|
+
: manager.policy;
|
|
54
|
+
BOOL managed = haveIdentity ? [manager isAccountIdManaged:primaryAccountId]
|
|
55
|
+
: manager.isManagementEnabled;
|
|
56
|
+
|
|
57
|
+
if (policy == nil) {
|
|
58
|
+
// Unmanaged is not an error: an app with no policy is fully permissive, and
|
|
59
|
+
// reporting `false` everywhere would hide functionality nothing is restricting.
|
|
60
|
+
return @{
|
|
61
|
+
@"isManaged" : @NO,
|
|
62
|
+
@"canSaveToLocal" : @YES,
|
|
63
|
+
@"canSaveToPersonal" : @YES,
|
|
64
|
+
@"canOpenFromUnmanaged" : @YES,
|
|
65
|
+
@"screenshotAllowed" : @YES,
|
|
66
|
+
@"raw" : @{
|
|
67
|
+
@"hasPrimaryAccount" : haveIdentity ? @"true" : @"false",
|
|
68
|
+
@"policySource" : RNIntunePolicySourceName(manager.mamPolicySource),
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// `withAccountId:nil` means the current identity. Single identity is decided, so there
|
|
74
|
+
// is never another one to ask about (SPEC §9, CLAUDE.md rule 10).
|
|
75
|
+
BOOL saveLocal = [policy isSaveToAllowedForLocation:IntuneMAMSaveLocationLocalDrive
|
|
76
|
+
withAccountId:nil];
|
|
77
|
+
BOOL savePersonal = [policy isSaveToAllowedForLocation:IntuneMAMSaveLocationOther
|
|
78
|
+
withAccountId:nil];
|
|
79
|
+
BOOL openUnmanaged = [policy isOpenFromAllowedForLocation:IntuneMAMOpenLocationOther
|
|
80
|
+
withAccountId:nil];
|
|
81
|
+
|
|
82
|
+
// Everything else the SDK reports, as strings. Explicitly outside semver: anything
|
|
83
|
+
// depended on here has to be promoted to a typed field first (SPEC §4.3).
|
|
84
|
+
NSDictionary<NSString *, NSString *> *raw = @{
|
|
85
|
+
@"isPINRequired" : policy.isPINRequired ? @"true" : @"false",
|
|
86
|
+
@"isManagedBrowserRequired" : policy.isManagedBrowserRequired ? @"true" : @"false",
|
|
87
|
+
@"isContactSyncAllowed" : policy.isContactSyncAllowed ? @"true" : @"false",
|
|
88
|
+
@"isSpotlightIndexingAllowed" : policy.isSpotlightIndexingAllowed ? @"true" : @"false",
|
|
89
|
+
@"areSiriIntentsAllowed" : policy.areSiriIntentsAllowed ? @"true" : @"false",
|
|
90
|
+
@"isAppSharingAllowed" : policy.isAppSharingAllowed ? @"true" : @"false",
|
|
91
|
+
@"isFileEncryptionRequired" : policy.isFileEncryptionRequired ? @"true" : @"false",
|
|
92
|
+
@"notificationPolicy" :
|
|
93
|
+
[NSString stringWithFormat:@"%ld", (long)policy.notificationPolicy],
|
|
94
|
+
// Diagnostic, not policy. An all-permissive snapshot means one of two things, and
|
|
95
|
+
// these two values say which: no identity resolved, or a policy that came from
|
|
96
|
+
// somewhere other than the MAM channel. Boolean only — the account ID itself must
|
|
97
|
+
// not travel here (CLAUDE.md rule 3).
|
|
98
|
+
@"hasPrimaryAccount" : haveIdentity ? @"true" : @"false",
|
|
99
|
+
@"policySource" : RNIntunePolicySourceName(manager.mamPolicySource),
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
return @{
|
|
103
|
+
@"isManaged" : @(managed),
|
|
104
|
+
@"canSaveToLocal" : @(saveLocal),
|
|
105
|
+
@"canSaveToPersonal" : @(savePersonal),
|
|
106
|
+
@"canOpenFromUnmanaged" : @(openUnmanaged),
|
|
107
|
+
@"screenshotAllowed" : @(policy.isScreenCaptureAllowed),
|
|
108
|
+
@"raw" : raw,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
@end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
//
|
|
2
|
+
// The reset sequence (SPEC §7).
|
|
3
|
+
//
|
|
4
|
+
// Separate from RNIntuneResetJournal on purpose: the journal is storage, this is the
|
|
5
|
+
// order of operations. The order is the part that is easy to get wrong — the unregister
|
|
6
|
+
// has to precede any token purge, and the process is not guaranteed to survive it.
|
|
7
|
+
//
|
|
8
|
+
|
|
9
|
+
#import <Foundation/Foundation.h>
|
|
10
|
+
|
|
11
|
+
@class RNIntuneAuth;
|
|
12
|
+
|
|
13
|
+
NS_ASSUME_NONNULL_BEGIN
|
|
14
|
+
|
|
15
|
+
@interface RNIntuneReset : NSObject
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Opens the journal, then unregisters and unenrolls every registered account.
|
|
19
|
+
*
|
|
20
|
+
* **Blocks** while the SDK acquires the Intune AAD token, so it must not run on the main
|
|
21
|
+
* thread. The caller is responsible for that.
|
|
22
|
+
*/
|
|
23
|
+
+ (void)runWithWipe:(BOOL)wipe
|
|
24
|
+
reason:(NSString *)reason
|
|
25
|
+
tenantId:(nullable NSString *)tenantId
|
|
26
|
+
auth:(nullable RNIntuneAuth *)auth;
|
|
27
|
+
|
|
28
|
+
/// Verifies the account is really gone and closes the journal. NO leaves it open, so the
|
|
29
|
+
/// next launch retries rather than declaring a reset that did not happen.
|
|
30
|
+
+ (BOOL)complete;
|
|
31
|
+
|
|
32
|
+
@end
|
|
33
|
+
|
|
34
|
+
NS_ASSUME_NONNULL_END
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright (c) 2026 react-native-intune contributors. MIT.
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
#import "RNIntuneReset.h"
|
|
6
|
+
#import "RNIntuneCore.h"
|
|
7
|
+
#import "RNIntuneResetJournal.h"
|
|
8
|
+
|
|
9
|
+
#import <IntuneMAMSwift/IntuneMAMSwift.h>
|
|
10
|
+
|
|
11
|
+
@implementation RNIntuneReset
|
|
12
|
+
|
|
13
|
+
+ (void)runWithWipe:(BOOL)wipe
|
|
14
|
+
reason:(NSString *)reason
|
|
15
|
+
tenantId:(NSString *)tenantId
|
|
16
|
+
auth:(RNIntuneAuth *)auth
|
|
17
|
+
{
|
|
18
|
+
IntuneMAMEnrollmentManager *manager =
|
|
19
|
+
RNIntuneCore.shared.sdkAvailable ? IntuneMAMEnrollmentManager.instance : nil;
|
|
20
|
+
|
|
21
|
+
// Every *registered* account, not just the enrolled one. An account whose enrollment
|
|
22
|
+
// failed is still registered, and the SDK keeps retrying it on a 24-hour schedule
|
|
23
|
+
// until it is unregistered — so leaving those behind is precisely the "incomplete
|
|
24
|
+
// reset is actively harmful" case in SPEC §7. Found by a runtime test that left a
|
|
25
|
+
// failed registration behind and then could not close the journal.
|
|
26
|
+
NSArray<NSString *> *accountIds = [manager.registeredAccountIds copy] ?: @[];
|
|
27
|
+
NSString *enrolled = manager.enrolledAccountId;
|
|
28
|
+
|
|
29
|
+
// Written first, and forced to disk, because the next call may end the process.
|
|
30
|
+
[RNIntuneResetJournal.shared openWithAccountId:enrolled ?: accountIds.firstObject
|
|
31
|
+
tenantId:tenantId
|
|
32
|
+
wipe:wipe
|
|
33
|
+
reason:reason];
|
|
34
|
+
|
|
35
|
+
for (NSString *accountId in accountIds) {
|
|
36
|
+
if (accountId.length == 0) {
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
// Blocks while acquiring the Intune AAD token (SPEC §5.3). The caller runs this off
|
|
40
|
+
// the main thread.
|
|
41
|
+
[manager deRegisterAndUnenrollAccountId:accountId withWipe:wipe];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Reached only if the process survived. Clearing the overrides is what makes a tenant
|
|
45
|
+
// switch possible afterwards — they persist across restarts otherwise (SPEC §5.2).
|
|
46
|
+
[RNIntuneResetJournal.shared advanceToStage:RNIntuneResetStageCleaningAuth];
|
|
47
|
+
IntuneMAMSettings.aadClientIdOverride = nil;
|
|
48
|
+
IntuneMAMSettings.aadAuthorityUriOverride = nil;
|
|
49
|
+
IntuneMAMSettings.aadRedirectUriOverride = nil;
|
|
50
|
+
|
|
51
|
+
// MSAL cache cleanup, and it is not cosmetic: issue #539 reports
|
|
52
|
+
// `deRegisterAndUnenrollAccountId` leaving keychain state behind, and #464 a fresh
|
|
53
|
+
// install where the SDK reports no enrolled account yet the sign-in webview autofills a
|
|
54
|
+
// previously enrolled address and then fails on account mismatch. Both are residual
|
|
55
|
+
// MSAL state rather than MAM state.
|
|
56
|
+
//
|
|
57
|
+
// After the unregister, deliberately: that call blocks while it acquires the Intune AAD
|
|
58
|
+
// token, and the token comes from this very cache. Clearing it first would make the
|
|
59
|
+
// unregister fail and strand a registered account (SPEC §5.3).
|
|
60
|
+
//
|
|
61
|
+
// Every account rather than one, for the same reason the loop above unregisters them
|
|
62
|
+
// all — by this point there is no single account id left to be selective with.
|
|
63
|
+
//
|
|
64
|
+
// nil in `external` mode, where the cache is the host app's (SPEC §3.2).
|
|
65
|
+
[auth removeAllAccounts];
|
|
66
|
+
[auth invalidate];
|
|
67
|
+
|
|
68
|
+
[RNIntuneResetJournal.shared advanceToStage:RNIntuneResetStageCleaningLocal];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
+ (BOOL)complete
|
|
72
|
+
{
|
|
73
|
+
if (RNIntuneResetJournal.shared.entry == nil) {
|
|
74
|
+
return YES;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Verify rather than assume (SPEC §7 step 5). A reset that did not take must stay open
|
|
78
|
+
// and be retried, because the SDK resumes enrollment retries on its own schedule and a
|
|
79
|
+
// half-finished reset is worse than none.
|
|
80
|
+
if (RNIntuneCore.shared.sdkAvailable) {
|
|
81
|
+
IntuneMAMEnrollmentManager *manager = IntuneMAMEnrollmentManager.instance;
|
|
82
|
+
NSString *stillEnrolled = manager.enrolledAccountId;
|
|
83
|
+
NSArray *stillRegistered = manager.registeredAccountIds;
|
|
84
|
+
if (stillEnrolled.length > 0 || stillRegistered.count > 0) {
|
|
85
|
+
return NO;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
[RNIntuneResetJournal.shared close];
|
|
90
|
+
return YES;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
@end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
//
|
|
2
|
+
// The reset journal (SPEC §7).
|
|
3
|
+
//
|
|
4
|
+
// Unregistering an account that had policy enforced makes the SDK wipe that account's
|
|
5
|
+
// data, and **the process terminating is expected behaviour, not a crash**. Code after
|
|
6
|
+
// the unregister call does not run. So the sequence is not held in memory: each stage is
|
|
7
|
+
// written down before it is attempted, and the next launch reads what was in flight.
|
|
8
|
+
//
|
|
9
|
+
// IDLE -> UNREGISTERING -> CLEANING_AUTH -> CLEANING_LOCAL -> IDLE
|
|
10
|
+
//
|
|
11
|
+
// Pure Objective-C, no SDK dependency: the journal has to be readable at launch before
|
|
12
|
+
// anything else is configured.
|
|
13
|
+
//
|
|
14
|
+
|
|
15
|
+
#import <Foundation/Foundation.h>
|
|
16
|
+
|
|
17
|
+
NS_ASSUME_NONNULL_BEGIN
|
|
18
|
+
|
|
19
|
+
/// Stage strings. Must match `ResetStage` in src/types.ts.
|
|
20
|
+
extern NSString *const RNIntuneResetStageUnregistering;
|
|
21
|
+
extern NSString *const RNIntuneResetStageCleaningAuth;
|
|
22
|
+
extern NSString *const RNIntuneResetStageCleaningLocal;
|
|
23
|
+
|
|
24
|
+
@interface RNIntuneResetJournal : NSObject
|
|
25
|
+
|
|
26
|
+
+ (instancetype)shared;
|
|
27
|
+
|
|
28
|
+
/// The open entry, or nil when no reset is in flight. Keys: stage, accountId, tenantId,
|
|
29
|
+
/// wipe, reason, startedAt.
|
|
30
|
+
@property (nonatomic, readonly, nullable) NSDictionary<NSString *, id> *entry;
|
|
31
|
+
|
|
32
|
+
/// Current stage, or nil. This is what `getState().pendingReset` reports.
|
|
33
|
+
@property (nonatomic, readonly, nullable) NSString *stage;
|
|
34
|
+
|
|
35
|
+
/// Why the open reset started, or nil. Reported as `getState().pendingResetReason` so a
|
|
36
|
+
/// resume can pass the original reason on rather than flattening it to `'resume'`.
|
|
37
|
+
@property (nonatomic, readonly, nullable) NSString *reason;
|
|
38
|
+
|
|
39
|
+
/// Writes the entry at `UNREGISTERING`. Must return before the unregister call is made —
|
|
40
|
+
/// after it, there may be no process left to write anything.
|
|
41
|
+
- (void)openWithAccountId:(nullable NSString *)accountId
|
|
42
|
+
tenantId:(nullable NSString *)tenantId
|
|
43
|
+
wipe:(BOOL)wipe
|
|
44
|
+
reason:(NSString *)reason;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Opens the journal for a wipe the *service* started, unless one is already open.
|
|
48
|
+
*
|
|
49
|
+
* A service-initiated wipe is one of the callers of the reset path (SPEC §7, "one code
|
|
50
|
+
* path, many callers"), and it needs the journal for the same reason `reset()` does: the
|
|
51
|
+
* SDK terminates the process, so the consumer's cleanup may not finish and nothing else
|
|
52
|
+
* will come back for it. Without an entry there is no `pendingReset`, so the next launch
|
|
53
|
+
* has no idea anything happened.
|
|
54
|
+
*
|
|
55
|
+
* Returns NO when an entry already exists. A reset already in flight must not be
|
|
56
|
+
* overwritten — its `reason` and `accountId` are what the resumed sequence acts on.
|
|
57
|
+
*/
|
|
58
|
+
- (BOOL)openForServiceWipeWithAccountId:(nullable NSString *)accountId
|
|
59
|
+
tenantId:(nullable NSString *)tenantId;
|
|
60
|
+
|
|
61
|
+
- (void)advanceToStage:(NSString *)stage;
|
|
62
|
+
|
|
63
|
+
/// Only after the account is verified gone. An unverified reset stays open and is retried
|
|
64
|
+
/// on the next launch rather than being marked done (SPEC §7 step 5).
|
|
65
|
+
- (void)close;
|
|
66
|
+
|
|
67
|
+
@end
|
|
68
|
+
|
|
69
|
+
NS_ASSUME_NONNULL_END
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright (c) 2026 react-native-intune contributors. MIT.
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
#import "RNIntuneResetJournal.h"
|
|
6
|
+
|
|
7
|
+
NSString *const RNIntuneResetStageUnregistering = @"unregistering";
|
|
8
|
+
NSString *const RNIntuneResetStageCleaningAuth = @"cleaningAuth";
|
|
9
|
+
NSString *const RNIntuneResetStageCleaningLocal = @"cleaningLocal";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* NSUserDefaults rather than a file in the app container.
|
|
13
|
+
*
|
|
14
|
+
* SPEC §7 requires the journal to survive the wipe, and on iOS the SDK encrypts files in
|
|
15
|
+
* the container under policy. Defaults are written by the app's own process and are not
|
|
16
|
+
* part of the SDK's managed-file surface, so they survive both the wipe and the process
|
|
17
|
+
* death that follows it — which is the whole point.
|
|
18
|
+
*
|
|
19
|
+
* [verify] against a real wipe once a tenant exists. Process-death survival is exercised
|
|
20
|
+
* and works; wipe survival cannot be tested without a policy-targeted account, and it is
|
|
21
|
+
* the half that matters most.
|
|
22
|
+
*/
|
|
23
|
+
static NSString *const RNIntuneJournalKey = @"com.reactnativeintune.resetJournal";
|
|
24
|
+
|
|
25
|
+
@implementation RNIntuneResetJournal
|
|
26
|
+
|
|
27
|
+
+ (instancetype)shared
|
|
28
|
+
{
|
|
29
|
+
static RNIntuneResetJournal *shared = nil;
|
|
30
|
+
static dispatch_once_t onceToken;
|
|
31
|
+
dispatch_once(&onceToken, ^{
|
|
32
|
+
shared = [[self alloc] init];
|
|
33
|
+
});
|
|
34
|
+
return shared;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
- (nullable NSDictionary<NSString *, id> *)entry
|
|
38
|
+
{
|
|
39
|
+
id stored = [NSUserDefaults.standardUserDefaults objectForKey:RNIntuneJournalKey];
|
|
40
|
+
return [stored isKindOfClass:NSDictionary.class] ? stored : nil;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
- (nullable NSString *)stage
|
|
44
|
+
{
|
|
45
|
+
id stage = self.entry[@"stage"];
|
|
46
|
+
return [stage isKindOfClass:NSString.class] ? stage : nil;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
- (nullable NSString *)reason
|
|
50
|
+
{
|
|
51
|
+
id reason = self.entry[@"reason"];
|
|
52
|
+
// An empty string is not a reason; treat it as absent so callers get one nil to check.
|
|
53
|
+
return [reason isKindOfClass:NSString.class] && [reason length] > 0 ? reason : nil;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
- (void)openWithAccountId:(nullable NSString *)accountId
|
|
57
|
+
tenantId:(nullable NSString *)tenantId
|
|
58
|
+
wipe:(BOOL)wipe
|
|
59
|
+
reason:(NSString *)reason
|
|
60
|
+
{
|
|
61
|
+
NSDictionary *entry = @{
|
|
62
|
+
@"stage" : RNIntuneResetStageUnregistering,
|
|
63
|
+
@"accountId" : accountId ?: @"",
|
|
64
|
+
@"tenantId" : tenantId ?: @"",
|
|
65
|
+
@"wipe" : @(wipe),
|
|
66
|
+
@"reason" : reason ?: @"",
|
|
67
|
+
@"startedAt" : @([NSDate.date timeIntervalSince1970]),
|
|
68
|
+
};
|
|
69
|
+
[NSUserDefaults.standardUserDefaults setObject:entry forKey:RNIntuneJournalKey];
|
|
70
|
+
// Forced to disk before returning: the caller is about to make a call that may end the
|
|
71
|
+
// process, and an entry still sitting in memory is an entry that never existed.
|
|
72
|
+
[NSUserDefaults.standardUserDefaults synchronize];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
- (BOOL)openForServiceWipeWithAccountId:(NSString *)accountId
|
|
76
|
+
tenantId:(NSString *)tenantId
|
|
77
|
+
{
|
|
78
|
+
if (self.entry != nil) {
|
|
79
|
+
return NO;
|
|
80
|
+
}
|
|
81
|
+
// `wipe: YES` because the SDK is already wiping; the flag records what happened rather
|
|
82
|
+
// than requesting it. `reason` is what lets the consumer's handler tell an
|
|
83
|
+
// administrator revoking access from a user logging out (SPEC §7.4).
|
|
84
|
+
[self openWithAccountId:accountId tenantId:tenantId wipe:YES reason:@"remote_wipe"];
|
|
85
|
+
return YES;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
- (void)advanceToStage:(NSString *)stage
|
|
89
|
+
{
|
|
90
|
+
NSDictionary *current = self.entry;
|
|
91
|
+
if (current == nil) {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
NSMutableDictionary *next = [current mutableCopy];
|
|
95
|
+
next[@"stage"] = stage;
|
|
96
|
+
[NSUserDefaults.standardUserDefaults setObject:next forKey:RNIntuneJournalKey];
|
|
97
|
+
[NSUserDefaults.standardUserDefaults synchronize];
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
- (void)close
|
|
101
|
+
{
|
|
102
|
+
[NSUserDefaults.standardUserDefaults removeObjectForKey:RNIntuneJournalKey];
|
|
103
|
+
[NSUserDefaults.standardUserDefaults synchronize];
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
@end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Codegen spec — the ONLY file Codegen reads, and the only `Native*.ts` allowed in
|
|
5
|
+
* `jsSrcsDir`. An extra one is picked up and fails confusingly.
|
|
6
|
+
*
|
|
7
|
+
* This file is deliberately loose (CLAUDE.md rule 11). Codegen accepts only
|
|
8
|
+
* `boolean | number | string | Object | Array | Promise | void`: no literal unions, no
|
|
9
|
+
* optional parameters, no enums. So every union and every optional is normalized in
|
|
10
|
+
* `index.ts` before it crosses the bridge, and every aggregate return is `Object` here
|
|
11
|
+
* and a real type there. Nothing in the public API is `Object`.
|
|
12
|
+
*
|
|
13
|
+
* Editing this file means Codegen must re-run — `pod install` on iOS,
|
|
14
|
+
* `generateCodegenArtifactsFromSchema` on Android. A native build that breaks right
|
|
15
|
+
* after a change here is a stale Codegen artifact until proven otherwise.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { TurboModuleRegistry } from 'react-native';
|
|
19
|
+
export default TurboModuleRegistry.getEnforcing('RNIntune');
|
|
20
|
+
//# sourceMappingURL=NativeIntune.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"names":["TurboModuleRegistry","getEnforcing"],"sourceRoot":"../../src","sources":["NativeIntune.ts"],"mappings":";;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA,SAASA,mBAAmB,QAA0B,cAAc;AAkHpE,eAAeA,mBAAmB,CAACC,YAAY,CAAO,UAAU,CAAC","ignoreList":[]}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Sign-in, for `authMode: 'builtin'` — the module owns MSAL (SPEC §3).
|
|
5
|
+
*
|
|
6
|
+
* Two tokens exist and only one of them appears here: the MAM service token is acquired
|
|
7
|
+
* internally and never crosses the bridge. Everything returned by these calls is for
|
|
8
|
+
* *your* API's scopes (SPEC §3.5).
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import NativeIntune from "../NativeIntune.js";
|
|
12
|
+
import { assertAccountId } from "../internal/errors.js";
|
|
13
|
+
import { toAuthAccount, toAuthResult } from "../internal/decode.js";
|
|
14
|
+
import { enroll } from "./enrollment.js";
|
|
15
|
+
/**
|
|
16
|
+
* Uses the broker when one is present, which also gives device-wide SSO: a user already
|
|
17
|
+
* signed into Outlook usually gets a token with no prompt at all.
|
|
18
|
+
*/
|
|
19
|
+
export async function signIn(params = {}) {
|
|
20
|
+
return toAuthResult(await NativeIntune.signIn(params));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Cache-first. Rejects with `E_INTERACTION_REQUIRED` when a prompt is needed — the normal
|
|
25
|
+
* pattern is `signInSilent` first, `signIn` on that rejection.
|
|
26
|
+
*/
|
|
27
|
+
export async function signInSilent(params = {}) {
|
|
28
|
+
return toAuthResult(await NativeIntune.signInSilent(params));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** For your own API's scopes, after sign-in. */
|
|
32
|
+
export async function acquireToken(params) {
|
|
33
|
+
return toAuthResult(await NativeIntune.acquireToken(params));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Accounts with cached refresh tokens. Under single identity (SPEC §9) this holds at
|
|
38
|
+
* most one; more than one means state a reset should have cleared.
|
|
39
|
+
*/
|
|
40
|
+
export async function getAccounts() {
|
|
41
|
+
const accounts = await NativeIntune.getAccounts();
|
|
42
|
+
return accounts.map(toAuthAccount);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Removes the account from the MSAL cache. With `wipeIntune: true` it also runs the full
|
|
47
|
+
* reset sequence, which is what you want on logout — and which means **the process may
|
|
48
|
+
* terminate during this call on Android.**
|
|
49
|
+
*/
|
|
50
|
+
export function signOut(params) {
|
|
51
|
+
assertAccountId(params.accountId);
|
|
52
|
+
return NativeIntune.signOut(params);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** The common path in one call. Use the primitives when you need a step in between. */
|
|
56
|
+
export async function signInAndEnroll(params = {}) {
|
|
57
|
+
const auth = await signIn(params);
|
|
58
|
+
const enrollment = await enroll({
|
|
59
|
+
accountId: auth.accountId
|
|
60
|
+
});
|
|
61
|
+
return {
|
|
62
|
+
auth,
|
|
63
|
+
enrollment
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
//# sourceMappingURL=auth.js.map
|