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,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The text surgery the Expo config plugin performs, as pure functions.
|
|
3
|
+
*
|
|
4
|
+
* Separated from the plugin wiring for one reason: `@expo/config-plugins` is not
|
|
5
|
+
* installed unless the consumer uses Expo, so anything importing it cannot be tested
|
|
6
|
+
* here. These take a string and return a string, so they are tested directly — which
|
|
7
|
+
* matters, because two of them edit generated Kotlin and a bad regex silently produces
|
|
8
|
+
* a file that does not compile.
|
|
9
|
+
*
|
|
10
|
+
* Every function is idempotent. `expo prebuild` runs plugins against freshly generated
|
|
11
|
+
* files most of the time, but `prebuild` without `--clean` runs them against files a
|
|
12
|
+
* previous run already touched, so applying twice has to be a no-op.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const MARKER = 'react-native-intune';
|
|
16
|
+
|
|
17
|
+
const MAM_PLUGIN_BLOCK = `
|
|
18
|
+
// ${MARKER}: the MAM Gradle plugin rewrites bytecode across the app and every
|
|
19
|
+
// dependency. It belongs in the app module — applied to a library it rewrites only that
|
|
20
|
+
// library, builds cleanly, and protects nothing.
|
|
21
|
+
apply plugin: "com.microsoft.intune.mam"
|
|
22
|
+
|
|
23
|
+
intunemam {
|
|
24
|
+
report = true
|
|
25
|
+
verify = true
|
|
26
|
+
incremental = true
|
|
27
|
+
}
|
|
28
|
+
`;
|
|
29
|
+
|
|
30
|
+
/** Applies the MAM plugin to android/app/build.gradle. */
|
|
31
|
+
function withMamPluginApplied(contents) {
|
|
32
|
+
if (contents.includes('com.microsoft.intune.mam')) {
|
|
33
|
+
return contents;
|
|
34
|
+
}
|
|
35
|
+
return `${contents.trimEnd()}\n${MAM_PLUGIN_BLOCK}`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Adds the plugin jar and javassist to the root buildscript classpath.
|
|
40
|
+
*
|
|
41
|
+
* javassist's version must match the SDK exactly — the plugin uses it to rewrite
|
|
42
|
+
* bytecode, and a mismatch fails in ways that point at the rewritten class rather than
|
|
43
|
+
* at the version.
|
|
44
|
+
*/
|
|
45
|
+
function withMamClasspath(contents, { javassist = '3.29.2-GA' } = {}) {
|
|
46
|
+
if (contents.includes('com.microsoft.intune.mam.build')) {
|
|
47
|
+
return contents;
|
|
48
|
+
}
|
|
49
|
+
const lines = [
|
|
50
|
+
` // ${MARKER}: managed block`,
|
|
51
|
+
' classpath files("$rootDir/../node_modules/react-native-intune/vendor/android/GradlePlugin/com.microsoft.intune.mam.build.jar")',
|
|
52
|
+
` classpath "org.javassist:javassist:${javassist}"`,
|
|
53
|
+
].join('\n');
|
|
54
|
+
|
|
55
|
+
// Anchor on the buildscript's own dependencies block. Matching `dependencies {`
|
|
56
|
+
// anywhere would hit the app-level one in a single-file project and put a buildscript
|
|
57
|
+
// classpath where it does nothing.
|
|
58
|
+
const anchor = /(buildscript\s*\{[\s\S]*?dependencies\s*\{\s*\n)/;
|
|
59
|
+
if (!anchor.test(contents)) {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
return contents.replace(anchor, `$1${lines}\n`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Makes the generated Application class derive from MAMApplication and register the auth
|
|
67
|
+
* callback.
|
|
68
|
+
*
|
|
69
|
+
* This is the difference Expo makes. On bare React Native this file is the developer's
|
|
70
|
+
* own source, so `doctor` can only report it; under prebuild the file is generated, so
|
|
71
|
+
* there is nothing of theirs to overwrite. It is also one of the three omissions that
|
|
72
|
+
* build and run while protecting nothing, which is why automating it is worth the
|
|
73
|
+
* string surgery.
|
|
74
|
+
*
|
|
75
|
+
* The callback goes in `onCreate` deliberately: the MAM plugin rewrites `onCreate` into
|
|
76
|
+
* `onMAMCreate` at build time, so writing `onMAMCreate` here would leave a method the
|
|
77
|
+
* plugin never calls.
|
|
78
|
+
*
|
|
79
|
+
* Returns null when the file does not look the way we expect. A warning the consumer can
|
|
80
|
+
* act on beats a generated file that does not compile.
|
|
81
|
+
*/
|
|
82
|
+
function withMamApplication(contents, language) {
|
|
83
|
+
if (contents.includes('MAMApplication')) {
|
|
84
|
+
return contents;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const isKotlin = language === 'kt';
|
|
88
|
+
const superclass = isKotlin
|
|
89
|
+
? /(class\s+\w+\s*:\s*)Application(\s*\(\s*\))?/
|
|
90
|
+
: /(class\s+\w+\s+extends\s+)Application/;
|
|
91
|
+
if (!superclass.test(contents)) {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
let next = contents.replace(superclass, '$1MAMApplication()');
|
|
96
|
+
if (!isKotlin) {
|
|
97
|
+
next = next.replace(
|
|
98
|
+
/(class\s+\w+\s+extends\s+)MAMApplication\(\)/,
|
|
99
|
+
'$1MAMApplication'
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Imports. `android.app.Application` may still be referenced elsewhere in the file, so
|
|
104
|
+
// it is added alongside rather than replacing anything.
|
|
105
|
+
const imports = isKotlin
|
|
106
|
+
? [
|
|
107
|
+
'import com.microsoft.intune.mam.client.app.MAMApplication',
|
|
108
|
+
'import com.reactnativeintune.RNIntuneAuthCallback',
|
|
109
|
+
]
|
|
110
|
+
: [
|
|
111
|
+
'import com.microsoft.intune.mam.client.app.MAMApplication;',
|
|
112
|
+
'import com.reactnativeintune.RNIntuneAuthCallback;',
|
|
113
|
+
];
|
|
114
|
+
const missing = imports.filter((i) => !next.includes(i));
|
|
115
|
+
if (missing.length > 0) {
|
|
116
|
+
const lastImport = [...next.matchAll(/^import .*$/gm)].pop();
|
|
117
|
+
if (!lastImport) {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
const at = lastImport.index + lastImport[0].length;
|
|
121
|
+
next = `${next.slice(0, at)}\n${missing.join('\n')}${next.slice(at)}`;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (!/RNIntuneAuthCallback\s*\.\s*register/.test(next)) {
|
|
125
|
+
// After `super.onCreate()`, not at the top of the method. The superclass is what
|
|
126
|
+
// initialises the MAM machinery the callback registers with, so registering first
|
|
127
|
+
// is registering against nothing — and it fails the way everything here fails, by
|
|
128
|
+
// reporting a licensing-shaped enrollment status that retries quietly.
|
|
129
|
+
const onCreate = isKotlin
|
|
130
|
+
? /(override\s+fun\s+onCreate\s*\(\s*\)\s*\{[^\n]*\n\s*super\.onCreate\s*\(\s*\)\s*\n)/
|
|
131
|
+
: /(public\s+void\s+onCreate\s*\(\s*\)\s*\{[^\n]*\n\s*super\.onCreate\s*\(\s*\)\s*;\s*\n)/;
|
|
132
|
+
if (!onCreate.test(next)) {
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
const call = isKotlin
|
|
136
|
+
? ` // ${MARKER}: the MAM plugin rewrites onCreate into onMAMCreate at build\n` +
|
|
137
|
+
` // time, so this belongs here rather than in an onMAMCreate we write.\n` +
|
|
138
|
+
' RNIntuneAuthCallback.register(this)\n'
|
|
139
|
+
: ` // ${MARKER}: the MAM plugin rewrites onCreate into onMAMCreate at build\n` +
|
|
140
|
+
` // time, so this belongs here rather than in an onMAMCreate we write.\n` +
|
|
141
|
+
' RNIntuneAuthCallback.register(this);\n';
|
|
142
|
+
next = next.replace(onCreate, `$1${call}`);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return next;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
module.exports = {
|
|
149
|
+
MARKER,
|
|
150
|
+
withMamPluginApplied,
|
|
151
|
+
withMamClasspath,
|
|
152
|
+
withMamApplication,
|
|
153
|
+
};
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Expo config plugin.
|
|
3
|
+
*
|
|
4
|
+
* Why automation is right here and wrong everywhere else in this package: config
|
|
5
|
+
* plugins run during `expo prebuild`, against files prebuild has just **generated**.
|
|
6
|
+
* There is no developer edit to fight, no git history to churn, and nothing to
|
|
7
|
+
* re-apply on the next version bump — the whole reason `setup` refuses to run from
|
|
8
|
+
* postinstall (SPEC §12.7) simply does not apply.
|
|
9
|
+
*
|
|
10
|
+
* So Expo users get nearly the entire integration checklist automatically, including
|
|
11
|
+
* the Application subclass, which on bare React Native `doctor` can only report because
|
|
12
|
+
* the file belongs to the developer.
|
|
13
|
+
*
|
|
14
|
+
* Usage, in app.json / app.config.js:
|
|
15
|
+
*
|
|
16
|
+
* {
|
|
17
|
+
* "plugins": [
|
|
18
|
+
* ["react-native-intune", { "androidSignatureHash": "Xo8W...yr9iU=" }]
|
|
19
|
+
* ]
|
|
20
|
+
* }
|
|
21
|
+
*
|
|
22
|
+
* Two props. `maxFileProtectionLevel` writes the plist-only `MaxFileProtectionLevel`
|
|
23
|
+
* key, which matters for any app that reads its own files while the screen is locked —
|
|
24
|
+
* the SDK's default makes them unreadable about ten seconds after the device locks.
|
|
25
|
+
*
|
|
26
|
+
* The one that cannot be omitted is `androidSignatureHash`. It cannot be derived: an EAS
|
|
27
|
+
* build is signed with a keystore Expo manages, so the value comes from
|
|
28
|
+
* `eas credentials` and only the project owner can read it. Without it the Android
|
|
29
|
+
* redirect activity is skipped and a warning says so, rather than a plausible-looking
|
|
30
|
+
* activity being written with the wrong hash — which fails at the first sign-in with an
|
|
31
|
+
* error naming neither key.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
const {
|
|
35
|
+
AndroidConfig,
|
|
36
|
+
WarningAggregator,
|
|
37
|
+
createRunOncePlugin,
|
|
38
|
+
withAndroidManifest,
|
|
39
|
+
withAppBuildGradle,
|
|
40
|
+
withEntitlementsPlist,
|
|
41
|
+
withInfoPlist,
|
|
42
|
+
withMainApplication,
|
|
43
|
+
withProjectBuildGradle,
|
|
44
|
+
} = require('@expo/config-plugins');
|
|
45
|
+
|
|
46
|
+
const {
|
|
47
|
+
MARKER,
|
|
48
|
+
withMamApplication,
|
|
49
|
+
withMamClasspath,
|
|
50
|
+
withMamPluginApplied,
|
|
51
|
+
} = require('./transforms');
|
|
52
|
+
|
|
53
|
+
const pkg = require('../package.json');
|
|
54
|
+
|
|
55
|
+
const BROKER_PACKAGES = [
|
|
56
|
+
'com.microsoft.windowsintune.companyportal',
|
|
57
|
+
'com.azure.authenticator',
|
|
58
|
+
'com.microsoft.workaccount',
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
const QUERY_SCHEMES = ['msauthv2', 'msauthv3', 'companyportal'];
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* `MaxFileProtectionLevel` is plist-only — the SDK reads it at launch and there is no
|
|
65
|
+
* runtime setter, which is why `configure()` rejects the option when the plist disagrees
|
|
66
|
+
* instead of ignoring it. Prebuild generates the plist, so this is exactly the kind of
|
|
67
|
+
* key a config plugin should be writing.
|
|
68
|
+
*
|
|
69
|
+
* Keys are the `FileProtectionLevel` values from the public API; values are what the SDK
|
|
70
|
+
* expects to read.
|
|
71
|
+
*/
|
|
72
|
+
const FILE_PROTECTION = {
|
|
73
|
+
complete: 'NSFileProtectionComplete',
|
|
74
|
+
completeUnlessOpen: 'NSFileProtectionCompleteUnlessOpen',
|
|
75
|
+
completeUntilFirstUserAuthentication:
|
|
76
|
+
'NSFileProtectionCompleteUntilFirstUserAuthentication',
|
|
77
|
+
none: 'NSFileProtectionNone',
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const DEFAULT_MSAL_GROUP = 'com.microsoft.adalcache';
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* In this order, and the order is a requirement rather than a preference: without an
|
|
84
|
+
* explicit access group iOS writes to the *first* group in the entitlements, so a
|
|
85
|
+
* Microsoft group in that position would receive keychain items the app writes.
|
|
86
|
+
*
|
|
87
|
+
* `$(PRODUCT_BUNDLE_IDENTIFIER)` rather than a literal, because prebuild is where the
|
|
88
|
+
* bundle id is decided and it can differ per build profile.
|
|
89
|
+
*/
|
|
90
|
+
const keychainGroups = (msalGroup) => [
|
|
91
|
+
'$(AppIdentifierPrefix)$(PRODUCT_BUNDLE_IDENTIFIER)',
|
|
92
|
+
'$(AppIdentifierPrefix)com.microsoft.intune.mam',
|
|
93
|
+
`$(AppIdentifierPrefix)${msalGroup}`,
|
|
94
|
+
];
|
|
95
|
+
|
|
96
|
+
// ---------------------------------------------------------------- ios
|
|
97
|
+
|
|
98
|
+
const withKeychainGroups = (config, { keychainGroup }) =>
|
|
99
|
+
withEntitlementsPlist(config, (c) => {
|
|
100
|
+
const wanted = keychainGroups(keychainGroup);
|
|
101
|
+
const existing = c.modResults['keychain-access-groups'] ?? [];
|
|
102
|
+
const others = existing.filter((g) => !wanted.includes(g));
|
|
103
|
+
c.modResults['keychain-access-groups'] = [...wanted, ...others];
|
|
104
|
+
return c;
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
const withMsalUrlScheme = (config, { maxFileProtectionLevel, keychainGroup }) =>
|
|
108
|
+
withInfoPlist(config, (c) => {
|
|
109
|
+
const scheme = 'msauth.$(PRODUCT_BUNDLE_IDENTIFIER)';
|
|
110
|
+
const types = c.modResults.CFBundleURLTypes ?? [];
|
|
111
|
+
const already = types.some((t) =>
|
|
112
|
+
(t.CFBundleURLSchemes ?? []).includes(scheme)
|
|
113
|
+
);
|
|
114
|
+
if (!already) {
|
|
115
|
+
types.push({ CFBundleURLSchemes: [scheme] });
|
|
116
|
+
}
|
|
117
|
+
c.modResults.CFBundleURLTypes = types;
|
|
118
|
+
|
|
119
|
+
const queries = c.modResults.LSApplicationQueriesSchemes ?? [];
|
|
120
|
+
for (const s of QUERY_SCHEMES) {
|
|
121
|
+
if (!queries.includes(s)) {
|
|
122
|
+
queries.push(s);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
c.modResults.LSApplicationQueriesSchemes = queries;
|
|
126
|
+
|
|
127
|
+
// Identity is configured at runtime, and a plist key alongside a runtime override is
|
|
128
|
+
// a known cause of enrollment failures whose error names the authority rather than
|
|
129
|
+
// the conflict. `configure()` refuses to start when it finds one, so leaving these
|
|
130
|
+
// in place would produce a prebuild that cannot run.
|
|
131
|
+
//
|
|
132
|
+
// ADALCacheKeychainGroupOverride is deliberately NOT removed: it has no runtime
|
|
133
|
+
// equivalent and is the only way to set the keychain group (SPEC §5.1.4).
|
|
134
|
+
const settings = c.modResults.IntuneMAMSettings;
|
|
135
|
+
if (settings && typeof settings === 'object') {
|
|
136
|
+
for (const key of ['ADALClientId', 'ADALAuthority', 'ADALRedirectUri']) {
|
|
137
|
+
delete settings[key];
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// `configure({ keychainGroupOverride })` has no runtime equivalent for the SDK's own
|
|
142
|
+
// side of it: `ADALCacheKeychainGroupOverride` is read from the plist at launch, and
|
|
143
|
+
// `configure()` refuses the option when the two disagree. Prebuild regenerates the
|
|
144
|
+
// plist, so without this an Expo app could not use a custom group at all — it would
|
|
145
|
+
// fail with E_PLIST_CONFLICT and there would be nowhere to fix it.
|
|
146
|
+
if (keychainGroup !== DEFAULT_MSAL_GROUP) {
|
|
147
|
+
c.modResults.IntuneMAMSettings = {
|
|
148
|
+
...(c.modResults.IntuneMAMSettings ?? {}),
|
|
149
|
+
ADALCacheKeychainGroupOverride: keychainGroup,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (maxFileProtectionLevel) {
|
|
154
|
+
const value = FILE_PROTECTION[maxFileProtectionLevel];
|
|
155
|
+
if (!value) {
|
|
156
|
+
WarningAggregator.addWarningIOS(
|
|
157
|
+
MARKER,
|
|
158
|
+
`maxFileProtectionLevel "${maxFileProtectionLevel}" is not one of ` +
|
|
159
|
+
`${Object.keys(FILE_PROTECTION).join(', ')}, so MaxFileProtectionLevel was ` +
|
|
160
|
+
'not written. configure() will reject the option rather than ignore it.'
|
|
161
|
+
);
|
|
162
|
+
} else {
|
|
163
|
+
c.modResults.IntuneMAMSettings = {
|
|
164
|
+
...(settings ?? {}),
|
|
165
|
+
MaxFileProtectionLevel: value,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return c;
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
// ---------------------------------------------------------------- android
|
|
173
|
+
|
|
174
|
+
const withBrokerQueries = (config) =>
|
|
175
|
+
withAndroidManifest(config, (c) => {
|
|
176
|
+
const manifest = c.modResults.manifest;
|
|
177
|
+
manifest.queries = manifest.queries ?? [{}];
|
|
178
|
+
const block = manifest.queries[0];
|
|
179
|
+
block.package = block.package ?? [];
|
|
180
|
+
for (const name of BROKER_PACKAGES) {
|
|
181
|
+
const present = block.package.some((p) => p.$?.['android:name'] === name);
|
|
182
|
+
if (!present) {
|
|
183
|
+
block.package.push({ $: { 'android:name': name } });
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return c;
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
const withRedirectActivity = (config, { androidSignatureHash }) =>
|
|
190
|
+
withAndroidManifest(config, (c) => {
|
|
191
|
+
if (!androidSignatureHash) {
|
|
192
|
+
WarningAggregator.addWarningAndroid(
|
|
193
|
+
MARKER,
|
|
194
|
+
'androidSignatureHash was not provided, so the MSAL redirect activity was not ' +
|
|
195
|
+
'added and sign-in will fail. Read it from `eas credentials` (or your own ' +
|
|
196
|
+
'keystore) and pass it as a plugin prop.'
|
|
197
|
+
);
|
|
198
|
+
return c;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const app = AndroidConfig.Manifest.getMainApplicationOrThrow(c.modResults);
|
|
202
|
+
app.activity = app.activity ?? [];
|
|
203
|
+
const name = 'com.microsoft.identity.client.BrowserTabActivity';
|
|
204
|
+
if (app.activity.some((a) => a.$?.['android:name'] === name)) {
|
|
205
|
+
return c;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const packageName =
|
|
209
|
+
AndroidConfig.Package.getPackage(c) ?? c.android?.package;
|
|
210
|
+
|
|
211
|
+
app.activity.push({
|
|
212
|
+
'$': { 'android:name': name, 'android:exported': 'true' },
|
|
213
|
+
'intent-filter': [
|
|
214
|
+
{
|
|
215
|
+
action: [{ $: { 'android:name': 'android.intent.action.VIEW' } }],
|
|
216
|
+
category: [
|
|
217
|
+
{ $: { 'android:name': 'android.intent.category.DEFAULT' } },
|
|
218
|
+
{ $: { 'android:name': 'android.intent.category.BROWSABLE' } },
|
|
219
|
+
],
|
|
220
|
+
data: [
|
|
221
|
+
{
|
|
222
|
+
$: {
|
|
223
|
+
'android:scheme': 'msauth',
|
|
224
|
+
'android:host': packageName,
|
|
225
|
+
// The DECODED hash — "/" and "=" literal. The redirect URI registered in
|
|
226
|
+
// Entra percent-encodes them as %2F and %3D. Same value, two spellings,
|
|
227
|
+
// in two places that must agree.
|
|
228
|
+
'android:path': `/${decodeURIComponent(androidSignatureHash)}`,
|
|
229
|
+
},
|
|
230
|
+
},
|
|
231
|
+
],
|
|
232
|
+
},
|
|
233
|
+
],
|
|
234
|
+
});
|
|
235
|
+
return c;
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
const withGradle = (config) => {
|
|
239
|
+
let next = withAppBuildGradle(config, (c) => {
|
|
240
|
+
if (c.modResults.language !== 'groovy') {
|
|
241
|
+
WarningAggregator.addWarningAndroid(
|
|
242
|
+
MARKER,
|
|
243
|
+
'android/app/build.gradle is Kotlin DSL, which this plugin does not edit. Apply ' +
|
|
244
|
+
'the MAM plugin by hand — see `npx react-native-intune doctor`.'
|
|
245
|
+
);
|
|
246
|
+
return c;
|
|
247
|
+
}
|
|
248
|
+
c.modResults.contents = withMamPluginApplied(c.modResults.contents);
|
|
249
|
+
return c;
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
next = withProjectBuildGradle(next, (c) => {
|
|
253
|
+
if (c.modResults.language !== 'groovy') {
|
|
254
|
+
WarningAggregator.addWarningAndroid(
|
|
255
|
+
MARKER,
|
|
256
|
+
'android/build.gradle is Kotlin DSL, which this plugin does not edit. Add the ' +
|
|
257
|
+
'MAM plugin classpath by hand — see `npx react-native-intune doctor`.'
|
|
258
|
+
);
|
|
259
|
+
return c;
|
|
260
|
+
}
|
|
261
|
+
const result = withMamClasspath(c.modResults.contents);
|
|
262
|
+
if (result === null) {
|
|
263
|
+
WarningAggregator.addWarningAndroid(
|
|
264
|
+
MARKER,
|
|
265
|
+
'Could not find a buildscript { dependencies { } } block in ' +
|
|
266
|
+
'android/build.gradle, so the MAM plugin classpath was not added.'
|
|
267
|
+
);
|
|
268
|
+
return c;
|
|
269
|
+
}
|
|
270
|
+
c.modResults.contents = result;
|
|
271
|
+
return c;
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
return next;
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
const withMamApplicationClass = (config) =>
|
|
278
|
+
withMainApplication(config, (c) => {
|
|
279
|
+
const result = withMamApplication(
|
|
280
|
+
c.modResults.contents,
|
|
281
|
+
c.modResults.language
|
|
282
|
+
);
|
|
283
|
+
if (result === null) {
|
|
284
|
+
// Said loudly, because this is one of the omissions that builds and runs while
|
|
285
|
+
// leaving the app unprotected. A quiet skip here is the worst outcome available.
|
|
286
|
+
WarningAggregator.addWarningAndroid(
|
|
287
|
+
MARKER,
|
|
288
|
+
'Could not make MainApplication extend MAMApplication — its shape was not ' +
|
|
289
|
+
'recognised. THE APP WILL BUILD AND RUN WITHOUT PROTECTION until this is ' +
|
|
290
|
+
'done by hand. Run `npx react-native-intune doctor` for the exact change.'
|
|
291
|
+
);
|
|
292
|
+
return c;
|
|
293
|
+
}
|
|
294
|
+
c.modResults.contents = result;
|
|
295
|
+
return c;
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
// ---------------------------------------------------------------- entry
|
|
299
|
+
|
|
300
|
+
const withIntune = (config, props = {}) => {
|
|
301
|
+
const androidSignatureHash = props.androidSignatureHash ?? null;
|
|
302
|
+
const maxFileProtectionLevel = props.maxFileProtectionLevel ?? null;
|
|
303
|
+
const keychainGroup = props.keychainGroup ?? DEFAULT_MSAL_GROUP;
|
|
304
|
+
|
|
305
|
+
let next = config;
|
|
306
|
+
next = withKeychainGroups(next, { keychainGroup });
|
|
307
|
+
next = withMsalUrlScheme(next, { maxFileProtectionLevel, keychainGroup });
|
|
308
|
+
next = withBrokerQueries(next);
|
|
309
|
+
next = withRedirectActivity(next, { androidSignatureHash });
|
|
310
|
+
next = withGradle(next);
|
|
311
|
+
next = withMamApplicationClass(next);
|
|
312
|
+
return next;
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Run-once, so listing the plugin twice — easy to do when a config is assembled from
|
|
317
|
+
* shared fragments — does not apply everything twice. The transforms are idempotent
|
|
318
|
+
* anyway; this makes the intent explicit rather than relying on that.
|
|
319
|
+
*/
|
|
320
|
+
module.exports = createRunOncePlugin(withIntune, pkg.name, pkg.version);
|
package/scripts/cli.mjs
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* `npx react-native-intune <command>`.
|
|
4
|
+
*
|
|
5
|
+
* Three commands, and the split between them is the point (SPEC §12.7): `doctor` reads
|
|
6
|
+
* and reports, `setup` writes only when asked, `fetch-sdks` touches nothing but this
|
|
7
|
+
* package's own vendor directory. Nothing here ever runs from `postinstall` except
|
|
8
|
+
* `fetch-sdks`, which is why that one is safe to.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { spawnSync } from 'node:child_process';
|
|
12
|
+
import path from 'node:path';
|
|
13
|
+
import { fileURLToPath } from 'node:url';
|
|
14
|
+
|
|
15
|
+
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
16
|
+
|
|
17
|
+
const COMMANDS = {
|
|
18
|
+
doctor: {
|
|
19
|
+
script: 'doctor.mjs',
|
|
20
|
+
summary: 'report what your project still needs; changes nothing',
|
|
21
|
+
},
|
|
22
|
+
setup: {
|
|
23
|
+
script: 'setup.mjs',
|
|
24
|
+
summary: 'apply what can be applied, and print the rest',
|
|
25
|
+
},
|
|
26
|
+
'fetch-sdks': {
|
|
27
|
+
script: 'fetch-sdks.mjs',
|
|
28
|
+
summary: "download the pinned Microsoft SDKs into this package's vendor/",
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const [command, ...rest] = process.argv.slice(2);
|
|
33
|
+
|
|
34
|
+
if (!command || command === '--help' || command === '-h' || command === 'help') {
|
|
35
|
+
usage();
|
|
36
|
+
process.exit(command ? 0 : 1);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (command === '--version' || command === '-v') {
|
|
40
|
+
const pkg = await import('node:fs').then((fs) =>
|
|
41
|
+
JSON.parse(fs.readFileSync(path.join(HERE, '..', 'package.json'), 'utf8'))
|
|
42
|
+
);
|
|
43
|
+
process.stdout.write(`${pkg.version}\n`);
|
|
44
|
+
process.exit(0);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const entry = COMMANDS[command];
|
|
48
|
+
if (!entry) {
|
|
49
|
+
process.stderr.write(`Unknown command: ${command}\n\n`);
|
|
50
|
+
usage();
|
|
51
|
+
process.exit(1);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Spawned rather than imported so each command keeps its own exit code, and so a crash
|
|
55
|
+
// in one cannot take the router's error reporting with it.
|
|
56
|
+
const result = spawnSync(process.execPath, [path.join(HERE, entry.script), ...rest], {
|
|
57
|
+
stdio: 'inherit',
|
|
58
|
+
});
|
|
59
|
+
process.exit(result.status ?? 1);
|
|
60
|
+
|
|
61
|
+
function usage() {
|
|
62
|
+
const width = Math.max(...Object.keys(COMMANDS).map((k) => k.length));
|
|
63
|
+
process.stdout.write(
|
|
64
|
+
`\nreact-native-intune\n\n` +
|
|
65
|
+
`Usage: npx react-native-intune <command>\n\n` +
|
|
66
|
+
Object.entries(COMMANDS)
|
|
67
|
+
.map(([name, c]) => ` ${name.padEnd(width)} ${c.summary}\n`)
|
|
68
|
+
.join('') +
|
|
69
|
+
`\nStart with \`doctor\`. It is read-only, and most Intune setup mistakes fail\n` +
|
|
70
|
+
`later with an error naming the wrong cause.\n\n`
|
|
71
|
+
);
|
|
72
|
+
}
|