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.
Files changed (110) hide show
  1. package/LICENSE +27 -0
  2. package/NOTICE +49 -0
  3. package/README.md +164 -0
  4. package/RNIntune.podspec +69 -0
  5. package/android/build.gradle +152 -0
  6. package/android/consumer-rules.pro +28 -0
  7. package/android/src/main/AndroidManifest.xml +2 -0
  8. package/android/src/main/java/com/reactnativeintune/RNIntuneAuth.kt +330 -0
  9. package/android/src/main/java/com/reactnativeintune/RNIntuneAuthCallback.kt +195 -0
  10. package/android/src/main/java/com/reactnativeintune/RNIntuneConfig.kt +63 -0
  11. package/android/src/main/java/com/reactnativeintune/RNIntuneEventQueue.kt +50 -0
  12. package/android/src/main/java/com/reactnativeintune/RNIntuneModule.kt +752 -0
  13. package/android/src/main/java/com/reactnativeintune/RNIntuneMsalConfig.kt +78 -0
  14. package/android/src/main/java/com/reactnativeintune/RNIntuneNotifications.kt +187 -0
  15. package/android/src/main/java/com/reactnativeintune/RNIntunePackage.kt +33 -0
  16. package/android/src/main/java/com/reactnativeintune/RNIntunePolicy.kt +108 -0
  17. package/android/src/main/java/com/reactnativeintune/RNIntuneReset.kt +85 -0
  18. package/android/src/main/java/com/reactnativeintune/RNIntuneResetJournal.kt +140 -0
  19. package/app.plugin.js +10 -0
  20. package/ios/RNIntune.h +20 -0
  21. package/ios/RNIntune.mm +467 -0
  22. package/ios/RNIntuneAuth.h +121 -0
  23. package/ios/RNIntuneAuth.m +390 -0
  24. package/ios/RNIntuneCore.h +150 -0
  25. package/ios/RNIntuneCore.m +535 -0
  26. package/ios/RNIntuneDelegates.h +70 -0
  27. package/ios/RNIntuneDelegates.m +268 -0
  28. package/ios/RNIntunePendingRequests.h +57 -0
  29. package/ios/RNIntunePendingRequests.m +230 -0
  30. package/ios/RNIntunePlistGuard.h +37 -0
  31. package/ios/RNIntunePlistGuard.m +147 -0
  32. package/ios/RNIntunePolicy.h +25 -0
  33. package/ios/RNIntunePolicy.m +112 -0
  34. package/ios/RNIntuneReset.h +34 -0
  35. package/ios/RNIntuneReset.m +93 -0
  36. package/ios/RNIntuneResetJournal.h +69 -0
  37. package/ios/RNIntuneResetJournal.m +106 -0
  38. package/lib/module/NativeIntune.js +20 -0
  39. package/lib/module/NativeIntune.js.map +1 -0
  40. package/lib/module/api/auth.js +66 -0
  41. package/lib/module/api/auth.js.map +1 -0
  42. package/lib/module/api/enrollment.js +104 -0
  43. package/lib/module/api/enrollment.js.map +1 -0
  44. package/lib/module/api/events.js +50 -0
  45. package/lib/module/api/events.js.map +1 -0
  46. package/lib/module/api/lifecycle.js +73 -0
  47. package/lib/module/api/lifecycle.js.map +1 -0
  48. package/lib/module/api/policy.js +23 -0
  49. package/lib/module/api/policy.js.map +1 -0
  50. package/lib/module/api/tokenProvider.js +80 -0
  51. package/lib/module/api/tokenProvider.js.map +1 -0
  52. package/lib/module/index.js +82 -0
  53. package/lib/module/index.js.map +1 -0
  54. package/lib/module/internal/decode.js +153 -0
  55. package/lib/module/internal/decode.js.map +1 -0
  56. package/lib/module/internal/emitter.js +15 -0
  57. package/lib/module/internal/emitter.js.map +1 -0
  58. package/lib/module/internal/errors.js +32 -0
  59. package/lib/module/internal/errors.js.map +1 -0
  60. package/lib/module/package.json +1 -0
  61. package/lib/module/types.js +143 -0
  62. package/lib/module/types.js.map +1 -0
  63. package/lib/typescript/package.json +1 -0
  64. package/lib/typescript/src/NativeIntune.d.ts +120 -0
  65. package/lib/typescript/src/NativeIntune.d.ts.map +1 -0
  66. package/lib/typescript/src/api/auth.d.ts +37 -0
  67. package/lib/typescript/src/api/auth.d.ts.map +1 -0
  68. package/lib/typescript/src/api/enrollment.d.ts +54 -0
  69. package/lib/typescript/src/api/enrollment.d.ts.map +1 -0
  70. package/lib/typescript/src/api/events.d.ts +24 -0
  71. package/lib/typescript/src/api/events.d.ts.map +1 -0
  72. package/lib/typescript/src/api/lifecycle.d.ts +33 -0
  73. package/lib/typescript/src/api/lifecycle.d.ts.map +1 -0
  74. package/lib/typescript/src/api/policy.d.ts +14 -0
  75. package/lib/typescript/src/api/policy.d.ts.map +1 -0
  76. package/lib/typescript/src/api/tokenProvider.d.ts +18 -0
  77. package/lib/typescript/src/api/tokenProvider.d.ts.map +1 -0
  78. package/lib/typescript/src/index.d.ts +67 -0
  79. package/lib/typescript/src/index.d.ts.map +1 -0
  80. package/lib/typescript/src/internal/decode.d.ts +48 -0
  81. package/lib/typescript/src/internal/decode.d.ts.map +1 -0
  82. package/lib/typescript/src/internal/emitter.d.ts +11 -0
  83. package/lib/typescript/src/internal/emitter.d.ts.map +1 -0
  84. package/lib/typescript/src/internal/errors.d.ts +22 -0
  85. package/lib/typescript/src/internal/errors.d.ts.map +1 -0
  86. package/lib/typescript/src/types.d.ts +317 -0
  87. package/lib/typescript/src/types.d.ts.map +1 -0
  88. package/package.json +204 -0
  89. package/plugin/transforms.js +153 -0
  90. package/plugin/withIntune.js +320 -0
  91. package/scripts/cli.mjs +72 -0
  92. package/scripts/doctor.mjs +232 -0
  93. package/scripts/fetch-sdks.mjs +456 -0
  94. package/scripts/lib/checks.mjs +720 -0
  95. package/scripts/lib/project.mjs +196 -0
  96. package/scripts/setup.mjs +233 -0
  97. package/sdk-lock.json +27 -0
  98. package/sdk-versions.json +70 -0
  99. package/src/NativeIntune.ts +130 -0
  100. package/src/api/auth.ts +73 -0
  101. package/src/api/enrollment.ts +112 -0
  102. package/src/api/events.ts +87 -0
  103. package/src/api/lifecycle.ts +75 -0
  104. package/src/api/policy.ts +22 -0
  105. package/src/api/tokenProvider.ts +80 -0
  106. package/src/index.ts +127 -0
  107. package/src/internal/decode.ts +191 -0
  108. package/src/internal/emitter.ts +14 -0
  109. package/src/internal/errors.ts +43 -0
  110. 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);
@@ -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
+ }