react-native-intune 0.1.1 → 1.0.0-rc.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 (36) hide show
  1. package/README.md +8 -4
  2. package/RNIntune.podspec +3 -3
  3. package/android/build.gradle +46 -25
  4. package/android/src/main/java/com/reactnativeintune/RNIntunePackage.kt +11 -6
  5. package/android/src/main/java/com/reactnativeintune/RNIntuneResetJournal.kt +1 -1
  6. package/ios/RNIntunePlistGuard.m +1 -1
  7. package/ios/RNIntuneResetJournal.m +1 -1
  8. package/lib/module/api/tokenProvider.js +3 -0
  9. package/lib/module/api/tokenProvider.js.map +1 -1
  10. package/lib/module/index.js +1 -0
  11. package/lib/module/index.js.map +1 -1
  12. package/lib/module/internal/decode.js +23 -2
  13. package/lib/module/internal/decode.js.map +1 -1
  14. package/lib/module/types.js +37 -6
  15. package/lib/module/types.js.map +1 -1
  16. package/lib/typescript/src/api/tokenProvider.d.ts +3 -0
  17. package/lib/typescript/src/api/tokenProvider.d.ts.map +1 -1
  18. package/lib/typescript/src/index.d.ts +1 -0
  19. package/lib/typescript/src/index.d.ts.map +1 -1
  20. package/lib/typescript/src/internal/decode.d.ts.map +1 -1
  21. package/lib/typescript/src/types.d.ts +61 -11
  22. package/lib/typescript/src/types.d.ts.map +1 -1
  23. package/package.json +7 -8
  24. package/plugin/transforms.js +155 -2
  25. package/plugin/withIntune.js +27 -0
  26. package/scripts/doctor.mjs +7 -3
  27. package/scripts/lib/checks.mjs +483 -27
  28. package/scripts/lib/edits.mjs +329 -0
  29. package/scripts/lib/project.mjs +13 -0
  30. package/scripts/setup.mjs +59 -9
  31. package/sdk-lock.json +9 -8
  32. package/sdk-versions.json +125 -28
  33. package/src/api/tokenProvider.ts +3 -0
  34. package/src/index.ts +1 -0
  35. package/src/internal/decode.ts +27 -8
  36. package/src/types.ts +66 -15
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-intune",
3
- "version": "0.1.1",
3
+ "version": "1.0.0-rc.1",
4
4
  "description": "Microsoft Intune app protection policies (MAM) for React Native",
5
5
  "main": "./lib/module/index.js",
6
6
  "types": "./lib/typescript/src/index.d.ts",
@@ -22,7 +22,7 @@
22
22
  "app.plugin.js",
23
23
  "plugin",
24
24
  "scripts",
25
- "!scripts/verify-expo.mjs",
25
+ "!scripts/verify",
26
26
  "sdk-versions.json",
27
27
  "sdk-lock.json",
28
28
  "!vendor",
@@ -44,7 +44,8 @@
44
44
  ],
45
45
  "scripts": {
46
46
  "fetch-sdks": "node scripts/fetch-sdks.mjs",
47
- "verify:expo": "node scripts/verify-expo.mjs",
47
+ "verify:expo": "node scripts/verify/expo.mjs",
48
+ "verify:bare": "node scripts/verify/bare.mjs",
48
49
  "postinstall": "node scripts/fetch-sdks.mjs --soft --quiet",
49
50
  "example": "yarn workspace react-native-intune-example",
50
51
  "clean": "del-cli android/build example/android/build example/android/app/build example/ios/build lib",
@@ -100,9 +101,9 @@
100
101
  "typescript": "^6.0.3"
101
102
  },
102
103
  "peerDependencies": {
103
- "@expo/config-plugins": ">=7",
104
+ "@expo/config-plugins": ">=54.0.0",
104
105
  "react": "*",
105
- "react-native": "*"
106
+ "react-native": ">=0.74.0"
106
107
  },
107
108
  "workspaces": [
108
109
  "example",
@@ -197,9 +198,7 @@
197
198
  ],
198
199
  "version": "0.63.0"
199
200
  },
200
- "bin": {
201
- "react-native-intune": "scripts/cli.mjs"
202
- },
201
+ "bin": "scripts/cli.mjs",
203
202
  "engines": {
204
203
  "node": ">=18"
205
204
  },
@@ -27,9 +27,20 @@ intunemam {
27
27
  }
28
28
  `;
29
29
 
30
- /** Applies the MAM plugin to android/app/build.gradle. */
30
+ /**
31
+ * Applies the MAM plugin to android/app/build.gradle.
32
+ *
33
+ * The guard looks for an actual application at the start of a line, not the plugin id
34
+ * anywhere: commenting the apply out is the first thing anyone does when the bytecode
35
+ * rewrite breaks a library, and a guard that read the comment as applied would leave
36
+ * the app unprotected while `setup` reported success.
37
+ */
31
38
  function withMamPluginApplied(contents) {
32
- if (contents.includes('com.microsoft.intune.mam')) {
39
+ if (
40
+ /^[ \t]*(?:apply\s+plugin\s*:\s*|id\s*\(?\s*)["']com\.microsoft\.intune\.mam["']/m.test(
41
+ contents
42
+ )
43
+ ) {
33
44
  return contents;
34
45
  }
35
46
  return `${contents.trimEnd()}\n${MAM_PLUGIN_BLOCK}`;
@@ -187,9 +198,151 @@ function addImports(contents, imports) {
187
198
  return `${contents.slice(0, at)}\n${missing.join('\n')}${contents.slice(at)}`;
188
199
  }
189
200
 
201
+ /**
202
+ * Forwards the sign-in redirect to MSAL from the generated AppDelegate.
203
+ *
204
+ * The URL type alone is not enough. When the broker — Authenticator or Company Portal —
205
+ * finishes, it opens the app with the result, and unless the AppDelegate hands that URL
206
+ * to MSAL it arrives and is dropped: the sign-in never completes and nothing reports
207
+ * why. Both halves were needed on a device, and the bare example has always had this
208
+ * one; the Expo plugin wrote the URL type and not the forward.
209
+ *
210
+ * MSAL goes first in the `||` chain, so a redirect meant for it is consumed before
211
+ * Linking sees it; any other URL falls through unchanged.
212
+ *
213
+ * Returns null when there is no `open url` handler to extend — a warning beats an
214
+ * AppDelegate edited in the wrong place.
215
+ */
216
+ function withMsalResponseHandler(contents, language) {
217
+ if (/handleMSALResponse/.test(contents)) {
218
+ return contents;
219
+ }
220
+ const isSwift = language === 'swift';
221
+
222
+ const handler = isSwift
223
+ ? /(func\s+application\s*\(\s*_\s+\w+\s*:\s*UIApplication\s*,\s*open\s+url\s*:\s*URL\s*,\s*options\s*:[^)]*\)\s*->\s*Bool\s*\{\s*\n(\s*)return\s+)/
224
+ : /(-\s*\(BOOL\)\s*application\s*:\s*\(UIApplication\s*\*\)\s*\w+\s+openURL\s*:\s*\(NSURL\s*\*\)\s*url\s+options\s*:[^{]*\{\s*\n(\s*)return\s+)/;
225
+ if (!handler.test(contents)) {
226
+ return withNewMsalResponseHandler(contents, isSwift);
227
+ }
228
+ const forward = isSwift
229
+ ? 'MSALPublicClientApplication.handleMSALResponse(url, sourceApplication: options[.sourceApplication] as? String) || '
230
+ : '[MSALPublicClientApplication handleMSALResponse:url sourceApplication:options[UIApplicationOpenURLOptionsSourceApplicationKey]] || ';
231
+ let next = contents.replace(handler, `$1${forward}`);
232
+
233
+ const importLine = isSwift ? 'import MSAL' : '#import <MSAL/MSAL.h>';
234
+ const imports = isSwift
235
+ ? /^(?:\w+\s+)?import\s+\w+.*$/gm
236
+ : /^#import\s+.*$/gm;
237
+ const lastImport = [...next.matchAll(imports)].pop();
238
+ if (!lastImport) {
239
+ return null;
240
+ }
241
+ const at = lastImport.index + lastImport[0].length;
242
+ next = `${next.slice(0, at)}\n${importLine}${next.slice(at)}`;
243
+ return next;
244
+ }
245
+
246
+ /**
247
+ * React Native's own templates have no open-URL handler at all — only Expo's does — so
248
+ * for them the forward is a whole method. It goes at the end of the AppDelegate class,
249
+ * found by matching braces from the class declaration; string literals and comments are
250
+ * skipped so a brace in either cannot end the class early.
251
+ *
252
+ * No `override`: in these templates the class is a UIResponder conforming to
253
+ * UIApplicationDelegate, and the method is the protocol's, not a superclass's.
254
+ */
255
+ function withNewMsalResponseHandler(contents, isSwift) {
256
+ const importLine = isSwift ? 'import MSAL' : '#import <MSAL/MSAL.h>';
257
+ let next = contents;
258
+
259
+ if (isSwift) {
260
+ const declaration = /class\s+AppDelegate\b[^{]*\{/.exec(next);
261
+ if (!declaration) {
262
+ return null;
263
+ }
264
+ const end = matchingBrace(
265
+ next,
266
+ declaration.index + declaration[0].length - 1
267
+ );
268
+ if (end === null) {
269
+ return null;
270
+ }
271
+ const method = `
272
+ /// Hands the sign-in result back to MSAL. Without it the broker's reply reaches the app
273
+ /// and is dropped, and sign-in never completes. Added by react-native-intune setup.
274
+ func application(
275
+ _ app: UIApplication,
276
+ open url: URL,
277
+ options: [UIApplication.OpenURLOptionsKey: Any] = [:]
278
+ ) -> Bool {
279
+ MSALPublicClientApplication.handleMSALResponse(
280
+ url,
281
+ sourceApplication: options[.sourceApplication] as? String
282
+ )
283
+ }
284
+ `;
285
+ next = `${next.slice(0, end).replace(/\s*$/, '\n')}${method}${next.slice(end)}`;
286
+ } else {
287
+ const implementation = /@implementation\s+AppDelegate\b[^\n]*\n/.exec(next);
288
+ if (!implementation) {
289
+ return null;
290
+ }
291
+ const at = implementation.index + implementation[0].length;
292
+ const method = `
293
+ // Hands the sign-in result back to MSAL. Without it the broker's reply reaches the app and
294
+ // is dropped, and sign-in never completes. Added by react-native-intune setup.
295
+ - (BOOL)application:(UIApplication *)application
296
+ openURL:(NSURL *)url
297
+ options:(NSDictionary<UIApplicationOpenURLOptionsKey, id> *)options
298
+ {
299
+ return [MSALPublicClientApplication handleMSALResponse:url
300
+ sourceApplication:options[UIApplicationOpenURLOptionsSourceApplicationKey]];
301
+ }
302
+ `;
303
+ next = `${next.slice(0, at)}${method}${next.slice(at)}`;
304
+ }
305
+
306
+ const imports = isSwift
307
+ ? /^(?:\w+\s+)?import\s+\w+.*$/gm
308
+ : /^#import\s+.*$/gm;
309
+ const lastImport = [...next.matchAll(imports)].pop();
310
+ if (!lastImport) {
311
+ return null;
312
+ }
313
+ const at = lastImport.index + lastImport[0].length;
314
+ return `${next.slice(0, at)}\n${importLine}${next.slice(at)}`;
315
+ }
316
+
317
+ /** The index of the brace closing the one at `open`, skipping strings and comments. */
318
+ function matchingBrace(src, open) {
319
+ let depth = 0;
320
+ for (let i = open; i < src.length; i += 1) {
321
+ const c = src[i];
322
+ if (c === '/' && src[i + 1] === '/') {
323
+ i = src.indexOf('\n', i);
324
+ if (i === -1) return null;
325
+ } else if (c === '/' && src[i + 1] === '*') {
326
+ i = src.indexOf('*/', i + 2) + 1;
327
+ if (i === 0) return null;
328
+ } else if (c === '"') {
329
+ for (i += 1; i < src.length && src[i] !== '"'; i += 1) {
330
+ if (src[i] === '\\') i += 1;
331
+ }
332
+ } else if (c === '{') {
333
+ depth += 1;
334
+ } else if (c === '}') {
335
+ depth -= 1;
336
+ if (depth === 0) return i;
337
+ }
338
+ }
339
+ return null;
340
+ }
341
+
190
342
  module.exports = {
191
343
  MARKER,
192
344
  withMamPluginApplied,
193
345
  withMamClasspath,
194
346
  withMamApplication,
347
+ withMsalResponseHandler,
195
348
  };
@@ -37,6 +37,7 @@ const {
37
37
  createRunOncePlugin,
38
38
  withAndroidManifest,
39
39
  withAppBuildGradle,
40
+ withAppDelegate,
40
41
  withEntitlementsPlist,
41
42
  withInfoPlist,
42
43
  withMainApplication,
@@ -48,6 +49,7 @@ const {
48
49
  withMamApplication,
49
50
  withMamClasspath,
50
51
  withMamPluginApplied,
52
+ withMsalResponseHandler,
51
53
  } = require('./transforms');
52
54
 
53
55
  const pkg = require('../package.json');
@@ -169,6 +171,30 @@ const withMsalUrlScheme = (config, { maxFileProtectionLevel, keychainGroup }) =>
169
171
  return c;
170
172
  });
171
173
 
174
+ /**
175
+ * The other half of the URL type. Without it the broker's redirect reaches the app and
176
+ * is dropped, and sign-in never completes — see withMsalResponseHandler.
177
+ */
178
+ const withMsalRedirectForward = (config) =>
179
+ withAppDelegate(config, (c) => {
180
+ const result = withMsalResponseHandler(
181
+ c.modResults.contents,
182
+ c.modResults.language
183
+ );
184
+ if (result === null) {
185
+ WarningAggregator.addWarningIOS(
186
+ MARKER,
187
+ 'Could not forward the MSAL redirect in AppDelegate — no open-URL handler ' +
188
+ 'was recognised. Sign-in through Authenticator or Company Portal will not ' +
189
+ 'return to the app until this is done by hand. Run ' +
190
+ '`npx react-native-intune doctor` for the exact change.'
191
+ );
192
+ return c;
193
+ }
194
+ c.modResults.contents = result;
195
+ return c;
196
+ });
197
+
172
198
  // ---------------------------------------------------------------- android
173
199
 
174
200
  const withBrokerQueries = (config) =>
@@ -305,6 +331,7 @@ const withIntune = (config, props = {}) => {
305
331
  let next = config;
306
332
  next = withKeychainGroups(next, { keychainGroup });
307
333
  next = withMsalUrlScheme(next, { maxFileProtectionLevel, keychainGroup });
334
+ next = withMsalRedirectForward(next);
308
335
  next = withBrokerQueries(next);
309
336
  next = withRedirectActivity(next, { androidSignatureHash });
310
337
  next = withGradle(next);
@@ -116,7 +116,8 @@ function main() {
116
116
  2
117
117
  ) + '\n'
118
118
  );
119
- process.exit(s.blocking.length > 0 ? 1 : 0);
119
+ process.exitCode = s.blocking.length > 0 ? 1 : 0;
120
+ return;
120
121
  }
121
122
 
122
123
  process.stdout.write(`\nreact-native-intune doctor\n${project.root}\n`);
@@ -141,7 +142,7 @@ function main() {
141
142
  `licensed for Intune, that an App Protection Policy targets this app, and that\n` +
142
143
  `a broker is installed on the device. Enrollment needs all three.\n\n`
143
144
  );
144
- process.exit(0);
145
+ return;
145
146
  }
146
147
 
147
148
  if (s.silent.length > 0) {
@@ -182,7 +183,10 @@ function main() {
182
183
  }
183
184
 
184
185
  process.stdout.write('\n');
185
- process.exit(s.blocking.length > 0 ? 1 : 0);
186
+ // An exit code, not process.exit(): on macOS a write to a pipe is asynchronous, and
187
+ // exiting straight after a long report cut it off mid-way whenever doctor's output was
188
+ // piped — into a CI log, a file, or `| less`.
189
+ process.exitCode = s.blocking.length > 0 ? 1 : 0;
186
190
  }
187
191
 
188
192
  /**