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,232 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Reports what the consumer's project still needs, and changes nothing.
|
|
4
|
+
*
|
|
5
|
+
* The value is not the checklist — it is that most of these failures name the wrong
|
|
6
|
+
* cause when they happen. A missing keychain entitlement surfaces as an authorization
|
|
7
|
+
* error, a missing URL type as an infinite login loop with an empty log, an unregistered
|
|
8
|
+
* auth callback as a licensing-shaped status that retries quietly forever. Every one of
|
|
9
|
+
* them was found on a real device rather than by reading documentation.
|
|
10
|
+
*
|
|
11
|
+
* Usage:
|
|
12
|
+
* npx react-native-intune doctor
|
|
13
|
+
* npx react-native-intune doctor --json # machine-readable, for CI
|
|
14
|
+
*
|
|
15
|
+
* Exit code: 1 when anything blocking is missing or wrong, 0 otherwise. Two exclusions,
|
|
16
|
+
* both deliberate. Advisories never fail the run, because a tool whose non-zero exit can
|
|
17
|
+
* mean "nothing important" teaches people to ignore it — and the exit exists for the
|
|
18
|
+
* class of failure that leaves an app unprotected while looking fine. Checks that could
|
|
19
|
+
* not be inspected are reported and do not fail either — a Kotlin DSL build file this
|
|
20
|
+
* tool cannot parse should not break someone's CI — but they are never counted as
|
|
21
|
+
* passing.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { execFileSync } from 'node:child_process';
|
|
25
|
+
import { checks } from './lib/checks.mjs';
|
|
26
|
+
import { findProject, rel } from './lib/project.mjs';
|
|
27
|
+
|
|
28
|
+
const args = process.argv.slice(2);
|
|
29
|
+
const asJson = args.includes('--json');
|
|
30
|
+
|
|
31
|
+
const SEVERITY_LABEL = {
|
|
32
|
+
silent: 'leaves the app unprotected',
|
|
33
|
+
loud: 'breaks the build or the first sign-in',
|
|
34
|
+
advisory: 'advisory',
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export function runChecks(project) {
|
|
38
|
+
return checks
|
|
39
|
+
.filter((c) => project[c.platform] !== null)
|
|
40
|
+
.map((c) => {
|
|
41
|
+
let result;
|
|
42
|
+
try {
|
|
43
|
+
result = c.inspect(project);
|
|
44
|
+
} catch (e) {
|
|
45
|
+
// A check that throws is a bug in this tool, not a finding about the project.
|
|
46
|
+
// Say which, so nobody spends an afternoon on their manifest.
|
|
47
|
+
result = {
|
|
48
|
+
state: 'unknown',
|
|
49
|
+
detail: `check failed to run: ${e.message}`,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
return { check: c, ...result };
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function summarise(results) {
|
|
57
|
+
const failing = results.filter((r) => r.state === 'missing' || r.state === 'wrong');
|
|
58
|
+
return {
|
|
59
|
+
ok: results.filter((r) => r.state === 'ok').length,
|
|
60
|
+
failing,
|
|
61
|
+
/**
|
|
62
|
+
* What decides the exit code — advisories are excluded on purpose.
|
|
63
|
+
*
|
|
64
|
+
* `advisory` is defined as worth knowing and not blocking, so letting one fail a
|
|
65
|
+
* build would contradict the label and, worse, teach people that a non-zero exit
|
|
66
|
+
* from this tool can be ignored. That habit is exactly what must not form: the
|
|
67
|
+
* whole point of the non-zero exit is the `silent` class, where the app builds and
|
|
68
|
+
* runs while protecting nothing.
|
|
69
|
+
*/
|
|
70
|
+
blocking: failing.filter((r) => r.check.severity !== 'advisory'),
|
|
71
|
+
unknown: results.filter((r) => r.state === 'unknown'),
|
|
72
|
+
skipped: results.filter((r) => r.state === 'skip'),
|
|
73
|
+
// Ordered so the dangerous class is read first, whatever order the checks are in.
|
|
74
|
+
silent: failing.filter((r) => r.check.severity === 'silent'),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function main() {
|
|
79
|
+
const project = findProject();
|
|
80
|
+
|
|
81
|
+
if (project.isThisPackage) {
|
|
82
|
+
process.stdout.write(
|
|
83
|
+
`This is react-native-intune's own repository, not a project that consumes it.\n\n` +
|
|
84
|
+
`Applying the MAM Gradle plugin here is the mistake these commands exist to\n` +
|
|
85
|
+
`catch: it rewrites only the library, builds cleanly, and protects nothing.\n` +
|
|
86
|
+
`Run this from your app's project root instead.\n`
|
|
87
|
+
);
|
|
88
|
+
process.exit(1);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (!project.android && !project.ios) {
|
|
92
|
+
process.stdout.write(
|
|
93
|
+
`No native project found under ${project.root}.\n` +
|
|
94
|
+
`Run this from a React Native project root — the folder holding android/ or ios/.\n`
|
|
95
|
+
);
|
|
96
|
+
process.exit(1);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const results = runChecks(project);
|
|
100
|
+
const s = summarise(results);
|
|
101
|
+
|
|
102
|
+
if (asJson) {
|
|
103
|
+
process.stdout.write(
|
|
104
|
+
JSON.stringify(
|
|
105
|
+
{
|
|
106
|
+
root: project.root,
|
|
107
|
+
results: results.map((r) => ({
|
|
108
|
+
id: r.check.id,
|
|
109
|
+
platform: r.check.platform,
|
|
110
|
+
severity: r.check.severity,
|
|
111
|
+
state: r.state,
|
|
112
|
+
detail: r.detail ?? null,
|
|
113
|
+
})),
|
|
114
|
+
},
|
|
115
|
+
null,
|
|
116
|
+
2
|
|
117
|
+
) + '\n'
|
|
118
|
+
);
|
|
119
|
+
process.exit(s.blocking.length > 0 ? 1 : 0);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
process.stdout.write(`\nreact-native-intune doctor\n${project.root}\n`);
|
|
123
|
+
|
|
124
|
+
for (const platform of ['ios', 'android']) {
|
|
125
|
+
const forPlatform = results.filter((r) => r.check.platform === platform);
|
|
126
|
+
if (forPlatform.length === 0) {
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
process.stdout.write(`\n${platform === 'ios' ? 'iOS' : 'Android'}\n`);
|
|
130
|
+
for (const r of forPlatform) {
|
|
131
|
+
process.stdout.write(` ${glyph(r.state)} ${r.check.title}${suffix(r)}\n`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (s.failing.length === 0 && s.unknown.length === 0) {
|
|
136
|
+
process.stdout.write(
|
|
137
|
+
`\nEverything this tool can check is in place.\n\n` +
|
|
138
|
+
`It cannot check the parts that live outside your project: that the account is\n` +
|
|
139
|
+
`licensed for Intune, that an App Protection Policy targets this app, and that\n` +
|
|
140
|
+
`a broker is installed on the device. Enrollment needs all three.\n\n`
|
|
141
|
+
);
|
|
142
|
+
process.exit(0);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (s.silent.length > 0) {
|
|
146
|
+
process.stdout.write(
|
|
147
|
+
`\n${'─'.repeat(72)}\n` +
|
|
148
|
+
`${s.silent.length} of these build and run while leaving the app unprotected.\n` +
|
|
149
|
+
`Nothing fails, nothing logs, and everyone believes protection is in place.\n` +
|
|
150
|
+
`${'─'.repeat(72)}\n`
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
for (const r of [...s.silent, ...s.failing.filter((f) => !s.silent.includes(f))]) {
|
|
155
|
+
process.stdout.write(
|
|
156
|
+
`\n${r.check.title}\n` +
|
|
157
|
+
` ${SEVERITY_LABEL[r.check.severity]}${r.detail ? ` — ${r.detail}` : ''}\n\n` +
|
|
158
|
+
` ${r.check.why}\n\n` +
|
|
159
|
+
indent(r.check.instruction(project)) +
|
|
160
|
+
'\n'
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (s.unknown.length > 0) {
|
|
165
|
+
process.stdout.write(
|
|
166
|
+
`\nNot verified — check these by hand:\n` +
|
|
167
|
+
s.unknown
|
|
168
|
+
.map((r) => ` - ${r.check.title}${r.detail ? ` (${r.detail})` : ''}`)
|
|
169
|
+
.join('\n') +
|
|
170
|
+
'\n'
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const applicable = s.failing.filter((r) => typeof r.check.apply === 'function');
|
|
175
|
+
if (applicable.length > 0) {
|
|
176
|
+
process.stdout.write(
|
|
177
|
+
`\n${applicable.length} of the above can be applied for you:\n` +
|
|
178
|
+
` npx react-native-intune setup\n`
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
process.stdout.write('\n');
|
|
183
|
+
process.exit(s.blocking.length > 0 ? 1 : 0);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function glyph(state) {
|
|
187
|
+
return { ok: 'ok ', missing: 'MISS', wrong: 'WRONG', unknown: '? ', skip: '-- ' }[
|
|
188
|
+
state
|
|
189
|
+
];
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function suffix(r) {
|
|
193
|
+
if (r.state === 'ok') {
|
|
194
|
+
return r.detail ? ` (${r.detail})` : '';
|
|
195
|
+
}
|
|
196
|
+
if (r.state === 'skip') {
|
|
197
|
+
return r.detail ? ` ${r.detail}` : '';
|
|
198
|
+
}
|
|
199
|
+
return '';
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function indent(text) {
|
|
203
|
+
return text
|
|
204
|
+
.split('\n')
|
|
205
|
+
.map((l) => (l.length > 0 ? ` ${l}` : l))
|
|
206
|
+
.join('\n');
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** Exported for `setup`, which refuses to edit files the developer cannot revert. */
|
|
210
|
+
export function gitIsClean(root) {
|
|
211
|
+
try {
|
|
212
|
+
const out = execFileSync('git', ['status', '--porcelain'], {
|
|
213
|
+
cwd: root,
|
|
214
|
+
encoding: 'utf8',
|
|
215
|
+
// Not a git repository is an outcome this function reports, not an error the user
|
|
216
|
+
// needs to see git's own words for. Without this, "fatal: not a git repository"
|
|
217
|
+
// leaks to the terminal and reads like the tool broke.
|
|
218
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
219
|
+
});
|
|
220
|
+
return out.trim().length === 0;
|
|
221
|
+
} catch {
|
|
222
|
+
// Not a git repository, or no git. Absence of a safety net is not the same as a
|
|
223
|
+
// dirty tree, and refusing to run would be unhelpful — the caller decides.
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export { rel };
|
|
229
|
+
|
|
230
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
231
|
+
main();
|
|
232
|
+
}
|
|
@@ -0,0 +1,456 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Downloads the pinned Microsoft Intune App SDK builds into vendor/.
|
|
4
|
+
*
|
|
5
|
+
* Why this exists: the SDKs are Microsoft's, under Microsoft's licence terms, and are
|
|
6
|
+
* not ours to redistribute. They are never committed to this repository and never
|
|
7
|
+
* published in the npm tarball. This script fetches them at install time instead.
|
|
8
|
+
*
|
|
9
|
+
* Safe to run from postinstall: it only ever writes inside this package's own vendor/
|
|
10
|
+
* directory. It never touches the consumer's project files — that is what
|
|
11
|
+
* `react-native-intune setup` is for, and that one is never automatic.
|
|
12
|
+
*
|
|
13
|
+
* Usage:
|
|
14
|
+
* node scripts/fetch-sdks.mjs # fetch what is missing
|
|
15
|
+
* node scripts/fetch-sdks.mjs --force # re-fetch even if present
|
|
16
|
+
* node scripts/fetch-sdks.mjs --platform ios # ios | android
|
|
17
|
+
* node scripts/fetch-sdks.mjs --check # verify only, exit 1 if incomplete
|
|
18
|
+
* node scripts/fetch-sdks.mjs --soft # never exit non-zero (for postinstall)
|
|
19
|
+
* node scripts/fetch-sdks.mjs --print-layout # list the archive's artifacts and exit
|
|
20
|
+
* node scripts/fetch-sdks.mjs --print-layout-all # ... including every nested file
|
|
21
|
+
*
|
|
22
|
+
* Requires Node 18+ (global fetch) and the `tar` CLI.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { createHash } from 'node:crypto';
|
|
26
|
+
import { execFileSync } from 'node:child_process';
|
|
27
|
+
import fs from 'node:fs';
|
|
28
|
+
import os from 'node:os';
|
|
29
|
+
import path from 'node:path';
|
|
30
|
+
import { fileURLToPath } from 'node:url';
|
|
31
|
+
|
|
32
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
33
|
+
const VENDOR = path.join(ROOT, 'vendor');
|
|
34
|
+
/**
|
|
35
|
+
* Lives at the repository root, not inside vendor/, because vendor/ is gitignored and
|
|
36
|
+
* this file is meant to be committed: it records the resolved tag and a sha256 per
|
|
37
|
+
* artifact, so every developer and CI run can verify they built against identical
|
|
38
|
+
* binaries rather than whatever "latest" meant that day.
|
|
39
|
+
*/
|
|
40
|
+
const LOCK = path.join(ROOT, 'sdk-lock.json');
|
|
41
|
+
const CONFIG = path.join(ROOT, 'sdk-versions.json');
|
|
42
|
+
|
|
43
|
+
const args = process.argv.slice(2);
|
|
44
|
+
const has = (flag) => args.includes(flag);
|
|
45
|
+
const opt = (flag) => {
|
|
46
|
+
const i = args.indexOf(flag);
|
|
47
|
+
return i === -1 ? null : args[i + 1];
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const FORCE = has('--force');
|
|
51
|
+
/**
|
|
52
|
+
* Never fail the process. Used by postinstall: a library must remain installable
|
|
53
|
+
* without network access or a GitHub token. The build is where a missing SDK must
|
|
54
|
+
* fail loudly — the podspec raises, and Gradle checks before compiling.
|
|
55
|
+
*/
|
|
56
|
+
const SOFT = has('--soft');
|
|
57
|
+
const CHECK_ONLY = has('--check');
|
|
58
|
+
const PRINT_LAYOUT = has('--print-layout') || has('--print-layout-all');
|
|
59
|
+
const PRINT_ALL = has('--print-layout-all');
|
|
60
|
+
const ONLY = opt('--platform');
|
|
61
|
+
const QUIET = has('--quiet') || process.env.npm_config_loglevel === 'silent';
|
|
62
|
+
|
|
63
|
+
const log = (...m) => !QUIET && console.log('[react-native-intune]', ...m);
|
|
64
|
+
const warn = (...m) => console.warn('[react-native-intune]', ...m);
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Built from the repositories actually being downloaded, not hardcoded: a licence notice
|
|
68
|
+
* that points somewhere other than the source of the bytes is worse than no notice.
|
|
69
|
+
*/
|
|
70
|
+
const licenceNotice = (cfg) => `
|
|
71
|
+
The Microsoft Intune App SDK is a Microsoft product, licensed by Microsoft under its
|
|
72
|
+
own terms. This library neither includes nor relicenses it, and downloading it here
|
|
73
|
+
does not grant you any rights to it. Review and accept Microsoft's terms before use:
|
|
74
|
+
iOS https://github.com/${cfg.ios.repo}
|
|
75
|
+
Android https://github.com/${cfg.android.repo}
|
|
76
|
+
`;
|
|
77
|
+
|
|
78
|
+
// ---------------------------------------------------------------- helpers
|
|
79
|
+
|
|
80
|
+
function readConfig() {
|
|
81
|
+
if (!fs.existsSync(CONFIG)) fail(`sdk-versions.json not found at ${CONFIG}`);
|
|
82
|
+
const cfg = JSON.parse(fs.readFileSync(CONFIG, 'utf8'));
|
|
83
|
+
for (const p of ['ios', 'android']) {
|
|
84
|
+
if (!cfg[p]) fail(`sdk-versions.json is missing the "${p}" section`);
|
|
85
|
+
if (cfg[p].tag === 'LATEST') {
|
|
86
|
+
warn(
|
|
87
|
+
`"${p}.tag" is still LATEST. Pin a real release tag before shipping — an ` +
|
|
88
|
+
`unpinned SDK means two developers can build against different binaries.`
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return cfg;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function readLock() {
|
|
96
|
+
try {
|
|
97
|
+
return JSON.parse(fs.readFileSync(LOCK, 'utf8'));
|
|
98
|
+
} catch {
|
|
99
|
+
return {};
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function writeLock(lock) {
|
|
104
|
+
fs.writeFileSync(LOCK, JSON.stringify(lock, null, 2) + '\n');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function fail(msg) {
|
|
108
|
+
if (SOFT) {
|
|
109
|
+
console.warn(
|
|
110
|
+
`\n[react-native-intune] Could not fetch the Microsoft SDKs yet.\n\n` +
|
|
111
|
+
msg
|
|
112
|
+
.split('\n')
|
|
113
|
+
.map((l) => ` ${l}`)
|
|
114
|
+
.join('\n') +
|
|
115
|
+
`\n\n Installation continues. Run \`yarn fetch-sdks\` before building —\n` +
|
|
116
|
+
` the iOS pod install and the Android build will fail with instructions if it is missing.\n`
|
|
117
|
+
);
|
|
118
|
+
process.exit(0);
|
|
119
|
+
}
|
|
120
|
+
console.error(`\n[react-native-intune] ${msg}\n`);
|
|
121
|
+
process.exit(1);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Turn a simple `**\/name` or `**\/name*.jar` pattern into a RegExp against a POSIX path.
|
|
126
|
+
*
|
|
127
|
+
* Tokens are substituted via placeholders rather than sequential string replacement:
|
|
128
|
+
* replacing `*` after inserting the `**\/` expansion would corrupt the `*` inside it,
|
|
129
|
+
* which silently breaks matching for nested paths.
|
|
130
|
+
*/
|
|
131
|
+
function globToRegExp(glob) {
|
|
132
|
+
const ANY_DIRS = '\u0000';
|
|
133
|
+
const ANY_SEG = '\u0001';
|
|
134
|
+
const body = glob
|
|
135
|
+
.replace(/\*\*\//g, ANY_DIRS)
|
|
136
|
+
.replace(/\*/g, ANY_SEG)
|
|
137
|
+
.replace(/[.+^${}()|[\]\\?]/g, '\\$&')
|
|
138
|
+
.split(ANY_DIRS)
|
|
139
|
+
.join('(?:[^\\0]*/)?')
|
|
140
|
+
.split(ANY_SEG)
|
|
141
|
+
.join('[^/]*');
|
|
142
|
+
return new RegExp(`^${body}$`);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function walk(dir, base = dir, out = []) {
|
|
146
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
147
|
+
const abs = path.join(dir, entry.name);
|
|
148
|
+
const rel = path.relative(base, abs).split(path.sep).join('/');
|
|
149
|
+
// An .xcframework is a directory but must be treated as a single artifact.
|
|
150
|
+
if (entry.isDirectory() && !entry.name.endsWith('.xcframework')) {
|
|
151
|
+
out.push({ rel, abs, dir: true });
|
|
152
|
+
walk(abs, base, out);
|
|
153
|
+
} else {
|
|
154
|
+
out.push({ rel, abs, dir: entry.isDirectory() });
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return out;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function sha256OfPath(target) {
|
|
161
|
+
const hash = createHash('sha256');
|
|
162
|
+
const add = (p) => {
|
|
163
|
+
const st = fs.statSync(p);
|
|
164
|
+
if (st.isDirectory()) {
|
|
165
|
+
for (const name of fs.readdirSync(p).sort()) add(path.join(p, name));
|
|
166
|
+
} else {
|
|
167
|
+
hash.update(path.basename(p));
|
|
168
|
+
hash.update(fs.readFileSync(p));
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
add(target);
|
|
172
|
+
return hash.digest('hex');
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function copyInto(from, toRel, { executable = false } = {}) {
|
|
176
|
+
const isDirTarget = toRel.endsWith('/');
|
|
177
|
+
const dest = isDirTarget
|
|
178
|
+
? path.join(VENDOR, toRel, path.basename(from))
|
|
179
|
+
: path.join(VENDOR, toRel);
|
|
180
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
181
|
+
fs.rmSync(dest, { recursive: true, force: true });
|
|
182
|
+
fs.cpSync(from, dest, { recursive: true });
|
|
183
|
+
if (executable) fs.chmodSync(dest, 0o755);
|
|
184
|
+
return dest;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Copy one named file out of a .dmg. No-op off macOS.
|
|
189
|
+
*
|
|
190
|
+
* The disk image is **converted before it is attached**, and that is the whole trick.
|
|
191
|
+
* Microsoft ships this one with a software license agreement attached
|
|
192
|
+
* (`hdiutil imageinfo` reports `Software License Agreement: true`), and `hdiutil attach`
|
|
193
|
+
* blocks on it — the mount is refused with "attach canceled" and the extraction silently
|
|
194
|
+
* produced nothing. That is why the configurator was never in `vendor/` despite this
|
|
195
|
+
* function existing and being wired up.
|
|
196
|
+
*
|
|
197
|
+
* Converting strips the agreement resource from the copy, so the copy attaches without a
|
|
198
|
+
* prompt. Nothing is auto-accepted on the user's behalf: `fetch-sdks` already asked them
|
|
199
|
+
* to accept Microsoft's terms before downloading anything, and this avoids clicking
|
|
200
|
+
* through a second agreement in their name rather than doing it for them.
|
|
201
|
+
*
|
|
202
|
+
* Returns the destination path, or null when extraction was not possible.
|
|
203
|
+
*/
|
|
204
|
+
function extractFromDmg(dmgPath, innerName, destRel) {
|
|
205
|
+
if (process.platform !== 'darwin') {
|
|
206
|
+
log(` (${path.basename(dmgPath)} kept as-is — extraction needs macOS)`);
|
|
207
|
+
return null;
|
|
208
|
+
}
|
|
209
|
+
let mountPoint = null;
|
|
210
|
+
let converted = null;
|
|
211
|
+
try {
|
|
212
|
+
converted = path.join(
|
|
213
|
+
fs.mkdtempSync(path.join(os.tmpdir(), 'rni-dmg-')),
|
|
214
|
+
'plain.dmg'
|
|
215
|
+
);
|
|
216
|
+
execFileSync('hdiutil', ['convert', dmgPath, '-format', 'UDRW', '-o', converted], {
|
|
217
|
+
stdio: 'pipe',
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
const out = execFileSync(
|
|
221
|
+
'hdiutil',
|
|
222
|
+
['attach', '-nobrowse', '-readonly', '-plist', converted],
|
|
223
|
+
{ encoding: 'utf8' }
|
|
224
|
+
);
|
|
225
|
+
// Cheap plist scrape: the mount point is the only /Volumes path in the output.
|
|
226
|
+
mountPoint = (out.match(/<string>(\/Volumes\/[^<]+)<\/string>/) ?? [])[1] ?? null;
|
|
227
|
+
if (!mountPoint) throw new Error('could not determine the mount point');
|
|
228
|
+
|
|
229
|
+
// Recursive, because the volume root is the .app bundle itself and the binary lives
|
|
230
|
+
// at Contents/MacOS/<name>.
|
|
231
|
+
const found = walk(mountPoint).find((e) => !e.dir && path.basename(e.rel) === innerName);
|
|
232
|
+
if (!found) throw new Error(`${innerName} was not inside the disk image`);
|
|
233
|
+
|
|
234
|
+
return copyInto(found.abs, destRel, { executable: true });
|
|
235
|
+
} catch (e) {
|
|
236
|
+
warn(
|
|
237
|
+
`Could not extract ${innerName} from ${path.basename(dmgPath)}: ${e.message}\n` +
|
|
238
|
+
` The disk image is at vendor/${path.relative(VENDOR, dmgPath)} — open it and copy\n` +
|
|
239
|
+
` ${innerName} to vendor/${destRel} by hand.`
|
|
240
|
+
);
|
|
241
|
+
return null;
|
|
242
|
+
} finally {
|
|
243
|
+
if (mountPoint) {
|
|
244
|
+
try {
|
|
245
|
+
execFileSync('hdiutil', ['detach', mountPoint, '-quiet'], { stdio: 'pipe' });
|
|
246
|
+
} catch {
|
|
247
|
+
/* leaving a volume mounted is not worth failing the install over */
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
if (converted) {
|
|
251
|
+
fs.rmSync(path.dirname(converted), { recursive: true, force: true });
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// ---------------------------------------------------------------- download
|
|
257
|
+
|
|
258
|
+
async function resolveTag(repo, tag) {
|
|
259
|
+
if (tag && tag !== 'LATEST') return tag;
|
|
260
|
+
const res = await fetch(`https://api.github.com/repos/${repo}/releases/latest`, {
|
|
261
|
+
headers: {
|
|
262
|
+
accept: 'application/vnd.github+json',
|
|
263
|
+
// A token lifts the very low unauthenticated rate limit. Optional.
|
|
264
|
+
...(process.env.GITHUB_TOKEN ? { authorization: `Bearer ${process.env.GITHUB_TOKEN}` } : {}),
|
|
265
|
+
},
|
|
266
|
+
});
|
|
267
|
+
if (!res.ok) {
|
|
268
|
+
fail(
|
|
269
|
+
`Could not resolve the latest release for ${repo} (HTTP ${res.status}).\n` +
|
|
270
|
+
` GitHub rate-limits unauthenticated requests aggressively. Either:\n` +
|
|
271
|
+
` - pin an explicit tag in sdk-versions.json (recommended), or\n` +
|
|
272
|
+
` - set GITHUB_TOKEN in your environment, or\n` +
|
|
273
|
+
` - download the SDK manually and place it under vendor/ (see README).`
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
const json = await res.json();
|
|
277
|
+
if (!json.tag_name) fail(`${repo} has no published releases to resolve.`);
|
|
278
|
+
return json.tag_name;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Download the source tarball for a tag and return the extraction directory.
|
|
283
|
+
*
|
|
284
|
+
* The tarball is used rather than release assets on purpose: both SDK repositories
|
|
285
|
+
* carry the binaries in the repository tree, and asset naming has changed across
|
|
286
|
+
* releases. Extracting the tree and locating artifacts by pattern is stable across
|
|
287
|
+
* both layouts, so this keeps working when Microsoft reorganises a release.
|
|
288
|
+
*/
|
|
289
|
+
async function downloadTree(repo, tag) {
|
|
290
|
+
const url = `https://codeload.github.com/${repo}/tar.gz/refs/tags/${encodeURIComponent(tag)}`;
|
|
291
|
+
log(`fetching ${repo}@${tag}`);
|
|
292
|
+
const res = await fetch(url);
|
|
293
|
+
if (!res.ok) {
|
|
294
|
+
fail(
|
|
295
|
+
`Download failed for ${repo}@${tag} (HTTP ${res.status}).\n` +
|
|
296
|
+
` Check that the tag exists: https://github.com/${repo}/tags`
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'rn-intune-'));
|
|
300
|
+
const tgz = path.join(tmp, 'sdk.tar.gz');
|
|
301
|
+
fs.writeFileSync(tgz, Buffer.from(await res.arrayBuffer()));
|
|
302
|
+
const out = path.join(tmp, 'src');
|
|
303
|
+
fs.mkdirSync(out);
|
|
304
|
+
try {
|
|
305
|
+
execFileSync('tar', ['-xzf', tgz, '-C', out, '--strip-components=1'], { stdio: 'pipe' });
|
|
306
|
+
} catch (e) {
|
|
307
|
+
fail(`Could not extract the archive. Is the \`tar\` CLI available?\n ${e.message}`);
|
|
308
|
+
}
|
|
309
|
+
fs.rmSync(tgz, { force: true });
|
|
310
|
+
return { dir: out, cleanup: () => fs.rmSync(tmp, { recursive: true, force: true }) };
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// ---------------------------------------------------------------- platform
|
|
314
|
+
|
|
315
|
+
async function fetchPlatform(name, cfg, lock) {
|
|
316
|
+
const spec = cfg[name];
|
|
317
|
+
const tag = await resolveTag(spec.repo, spec.tag);
|
|
318
|
+
|
|
319
|
+
const satisfied =
|
|
320
|
+
!FORCE &&
|
|
321
|
+
lock[name]?.tag === tag &&
|
|
322
|
+
spec.expect
|
|
323
|
+
.filter((e) => e.required)
|
|
324
|
+
.every((e) => fs.existsSync(path.join(VENDOR, e.as.replace(/\/$/, ''))));
|
|
325
|
+
|
|
326
|
+
if (satisfied) {
|
|
327
|
+
log(`${name}: already at ${tag}`);
|
|
328
|
+
return { name, tag, skipped: true };
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const { dir, cleanup } = await downloadTree(spec.repo, tag);
|
|
332
|
+
try {
|
|
333
|
+
const entries = walk(dir);
|
|
334
|
+
|
|
335
|
+
if (PRINT_LAYOUT) {
|
|
336
|
+
console.log(`\n--- ${spec.repo}@${tag} ---`);
|
|
337
|
+
// Top level first, then the bundle/framework artifacts, then everything else.
|
|
338
|
+
// Directories matter here: .xcframework and .bundle are single artifacts, and
|
|
339
|
+
// hiding them would omit exactly the entries the globs need to match.
|
|
340
|
+
const interesting = PRINT_ALL
|
|
341
|
+
? entries
|
|
342
|
+
: entries.filter(
|
|
343
|
+
(e) => !e.rel.includes('/') || /\.(xcframework|bundle|aar|jar|dmg)$/.test(e.rel)
|
|
344
|
+
);
|
|
345
|
+
for (const e of interesting.sort((a, b) => a.rel.localeCompare(b.rel))) {
|
|
346
|
+
console.log(` ${e.dir ? '[dir] ' : ' '}${e.rel}`);
|
|
347
|
+
}
|
|
348
|
+
const hidden = entries.length - interesting.length;
|
|
349
|
+
if (hidden > 0) console.log(` … ${hidden} nested files omitted (pass --print-layout-all to see them)`);
|
|
350
|
+
return { name, tag, printed: true };
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const resolved = {};
|
|
354
|
+
const missing = [];
|
|
355
|
+
|
|
356
|
+
for (const want of spec.expect) {
|
|
357
|
+
const re = globToRegExp(want.glob);
|
|
358
|
+
const hits = entries.filter((e) => re.test(e.rel));
|
|
359
|
+
if (hits.length === 0) {
|
|
360
|
+
if (want.required) missing.push(want.glob);
|
|
361
|
+
continue;
|
|
362
|
+
}
|
|
363
|
+
// Shallowest path wins: repositories often ship a copy inside a sample app too.
|
|
364
|
+
hits.sort((a, b) => a.rel.split('/').length - b.rel.split('/').length);
|
|
365
|
+
for (const hit of want.as.endsWith('/') ? hits : [hits[0]]) {
|
|
366
|
+
const dest = copyInto(hit.abs, want.as, { executable: want.executable });
|
|
367
|
+
resolved[path.relative(VENDOR, dest)] = sha256OfPath(dest);
|
|
368
|
+
log(` ${path.relative(VENDOR, dest)}`);
|
|
369
|
+
|
|
370
|
+
// Microsoft ships IntuneMAMConfigurator inside a .dmg. Mount it and take the
|
|
371
|
+
// binary out so consumers can call it from an Xcode build phase. macOS only —
|
|
372
|
+
// the dmg is kept either way so a Linux CI checkout is still complete.
|
|
373
|
+
if (want.extractDmg) {
|
|
374
|
+
const extracted = extractFromDmg(dest, want.extractDmg, want.extractAs);
|
|
375
|
+
if (extracted) {
|
|
376
|
+
resolved[path.relative(VENDOR, extracted)] = sha256OfPath(extracted);
|
|
377
|
+
log(` ${path.relative(VENDOR, extracted)}`);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
if (missing.length) {
|
|
384
|
+
fail(
|
|
385
|
+
`${spec.repo}@${tag} did not contain expected artifacts:\n` +
|
|
386
|
+
missing.map((m) => ` ${m}`).join('\n') +
|
|
387
|
+
`\n\n Microsoft may have changed the release layout. Run:\n` +
|
|
388
|
+
` node scripts/fetch-sdks.mjs --platform ${name} --print-layout\n` +
|
|
389
|
+
` and update the "expect" globs in sdk-versions.json to match.`
|
|
390
|
+
);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
return { name, tag, files: resolved };
|
|
394
|
+
} finally {
|
|
395
|
+
cleanup();
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// ---------------------------------------------------------------- check
|
|
400
|
+
|
|
401
|
+
function check(cfg, lock) {
|
|
402
|
+
const problems = [];
|
|
403
|
+
for (const name of ['ios', 'android']) {
|
|
404
|
+
if (ONLY && ONLY !== name) continue;
|
|
405
|
+
if (!lock[name]) {
|
|
406
|
+
problems.push(`${name}: not fetched`);
|
|
407
|
+
continue;
|
|
408
|
+
}
|
|
409
|
+
for (const want of cfg[name].expect.filter((e) => e.required)) {
|
|
410
|
+
const target = path.join(VENDOR, want.as.replace(/\/$/, ''));
|
|
411
|
+
if (!fs.existsSync(target)) problems.push(`${name}: missing ${want.as}`);
|
|
412
|
+
}
|
|
413
|
+
for (const [rel, expected] of Object.entries(lock[name].files ?? {})) {
|
|
414
|
+
const target = path.join(VENDOR, rel);
|
|
415
|
+
if (fs.existsSync(target) && sha256OfPath(target) !== expected) {
|
|
416
|
+
problems.push(`${name}: ${rel} has been modified since it was fetched`);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
if (problems.length) {
|
|
422
|
+
console.error('\n[react-native-intune] vendor/ is incomplete:\n');
|
|
423
|
+
for (const p of problems) console.error(` ${p}`);
|
|
424
|
+
console.error('\n Run: npx react-native-intune fetch-sdks\n');
|
|
425
|
+
process.exit(1);
|
|
426
|
+
}
|
|
427
|
+
log('vendor/ is complete');
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// ---------------------------------------------------------------- main
|
|
431
|
+
|
|
432
|
+
async function main() {
|
|
433
|
+
const cfg = readConfig();
|
|
434
|
+
const lock = readLock();
|
|
435
|
+
|
|
436
|
+
if (CHECK_ONLY) return check(cfg, lock);
|
|
437
|
+
|
|
438
|
+
const platforms = ['ios', 'android'].filter((p) => !ONLY || ONLY === p);
|
|
439
|
+
if (platforms.length === 0) fail(`--platform must be "ios" or "android", got "${ONLY}"`);
|
|
440
|
+
|
|
441
|
+
// Only iOS artifacts are needed on a machine that cannot build for iOS anyway,
|
|
442
|
+
// but fetching both keeps the lockfile meaningful and the download is one-time.
|
|
443
|
+
if (!QUIET && !PRINT_LAYOUT) console.log(licenceNotice(cfg));
|
|
444
|
+
|
|
445
|
+
for (const name of platforms) {
|
|
446
|
+
const result = await fetchPlatform(name, cfg, lock);
|
|
447
|
+
if (result.printed) continue;
|
|
448
|
+
if (result.skipped) continue;
|
|
449
|
+
lock[name] = { repo: cfg[name].repo, tag: result.tag, fetchedAt: new Date().toISOString(), files: result.files };
|
|
450
|
+
writeLock(lock);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
if (!PRINT_LAYOUT) log('done — vendor/ ready');
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
main().catch((e) => fail(e?.stack ?? String(e)));
|