@elizaos/capacitor-mobile-signals 1.0.0
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/ElizaosCapacitorMobileSignals.podspec +18 -0
- package/android/build.gradle +53 -0
- package/android/src/main/AndroidManifest.xml +8 -0
- package/android/src/main/java/ai/eliza/plugins/mobilesignals/MobileSignalsPlugin.kt +857 -0
- package/dist/esm/definitions.d.ts +162 -0
- package/dist/esm/definitions.d.ts.map +1 -0
- package/dist/esm/definitions.js +1 -0
- package/dist/esm/index.d.ts +4 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/index.js +6 -0
- package/dist/esm/web.d.ts +29 -0
- package/dist/esm/web.d.ts.map +1 -0
- package/dist/esm/web.js +272 -0
- package/dist/esm/web.test.d.ts +2 -0
- package/dist/esm/web.test.d.ts.map +1 -0
- package/dist/esm/web.test.js +75 -0
- package/dist/plugin.cjs.js +285 -0
- package/dist/plugin.cjs.js.map +1 -0
- package/dist/plugin.js +288 -0
- package/dist/plugin.js.map +1 -0
- package/ios/Sources/MobileSignalsPlugin/MobileSignalsPlugin.swift +968 -0
- package/ios/Sources/MobileSignalsPlugin/ScreenTimeSupport.swift +188 -0
- package/package.json +84 -0
- package/scripts/validate-ios-screen-time.mjs +320 -0
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import FamilyControls
|
|
2
|
+
import DeviceActivity
|
|
3
|
+
import Foundation
|
|
4
|
+
import Security
|
|
5
|
+
|
|
6
|
+
enum ScreenTimeSupport {
|
|
7
|
+
private static let familyControlsEntitlement = "com.apple.developer.family-controls"
|
|
8
|
+
private static let requiredFrameworks = ["FamilyControls", "DeviceActivity"]
|
|
9
|
+
|
|
10
|
+
private struct EntitlementInspection {
|
|
11
|
+
let familyControls: Bool
|
|
12
|
+
let inspected: String
|
|
13
|
+
let reason: String?
|
|
14
|
+
|
|
15
|
+
var satisfied: Bool {
|
|
16
|
+
familyControls
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
var canAttemptAuthorization: Bool {
|
|
20
|
+
inspected == "not-inspectable" || familyControls
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
static func buildStatus(reasonOverride: String? = nil) -> [String: Any] {
|
|
25
|
+
let entitlementInspection = inspectEntitlements()
|
|
26
|
+
let familyControlsEnabled = entitlementInspection.familyControls
|
|
27
|
+
let authorizationEntitlementAvailable = entitlementInspection.canAttemptAuthorization
|
|
28
|
+
let authorizationStatus = authorizationStatusString()
|
|
29
|
+
let provisioningSatisfied = entitlementInspection.satisfied
|
|
30
|
+
|
|
31
|
+
let reason = reasonOverride ?? derivedReason(
|
|
32
|
+
familyControlsEnabled: authorizationEntitlementAvailable,
|
|
33
|
+
authorizationStatus: authorizationStatus
|
|
34
|
+
)
|
|
35
|
+
let provisioningReason: Any = provisioningSatisfied
|
|
36
|
+
? NSNull()
|
|
37
|
+
: (entitlementInspection.reason ?? reason)
|
|
38
|
+
|
|
39
|
+
return [
|
|
40
|
+
"supported": provisioningSatisfied || entitlementInspection.inspected == "not-inspectable",
|
|
41
|
+
"requirements": [
|
|
42
|
+
"entitlements": [
|
|
43
|
+
"familyControls": familyControlsEntitlement,
|
|
44
|
+
],
|
|
45
|
+
"frameworks": requiredFrameworks,
|
|
46
|
+
"deviceActivityReportExtension": false,
|
|
47
|
+
"deviceActivityMonitorExtension": false,
|
|
48
|
+
],
|
|
49
|
+
"entitlements": [
|
|
50
|
+
"familyControls": familyControlsEnabled,
|
|
51
|
+
],
|
|
52
|
+
"provisioning": [
|
|
53
|
+
"satisfied": provisioningSatisfied,
|
|
54
|
+
"inspected": entitlementInspection.inspected,
|
|
55
|
+
"reason": provisioningReason,
|
|
56
|
+
],
|
|
57
|
+
"authorization": [
|
|
58
|
+
"status": authorizationStatus,
|
|
59
|
+
"canRequest": canRequestAuthorization(
|
|
60
|
+
familyControlsEnabled: authorizationEntitlementAvailable,
|
|
61
|
+
authorizationStatus: authorizationStatus
|
|
62
|
+
),
|
|
63
|
+
],
|
|
64
|
+
"reportAvailable": false,
|
|
65
|
+
"coarseSummaryAvailable": false,
|
|
66
|
+
"thresholdEventsAvailable": false,
|
|
67
|
+
"rawUsageExportAvailable": false,
|
|
68
|
+
"reason": reason,
|
|
69
|
+
]
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
static func requestAuthorizationIfAvailable(
|
|
73
|
+
completion: @escaping (String?) -> Void
|
|
74
|
+
) {
|
|
75
|
+
let entitlementInspection = inspectEntitlements()
|
|
76
|
+
let authorizationStatus = authorizationStatusString()
|
|
77
|
+
guard canRequestAuthorization(
|
|
78
|
+
familyControlsEnabled: entitlementInspection.canAttemptAuthorization,
|
|
79
|
+
authorizationStatus: authorizationStatus
|
|
80
|
+
) else {
|
|
81
|
+
DispatchQueue.main.async {
|
|
82
|
+
completion(nil)
|
|
83
|
+
}
|
|
84
|
+
return
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if #available(iOS 16.0, *) {
|
|
88
|
+
Task { @MainActor in
|
|
89
|
+
do {
|
|
90
|
+
try await AuthorizationCenter.shared.requestAuthorization(for: .individual)
|
|
91
|
+
completion(nil)
|
|
92
|
+
} catch {
|
|
93
|
+
completion("Screen Time authorization request failed: \(error.localizedDescription)")
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
DispatchQueue.main.async {
|
|
100
|
+
AuthorizationCenter.shared.requestAuthorization { result in
|
|
101
|
+
DispatchQueue.main.async {
|
|
102
|
+
switch result {
|
|
103
|
+
case .success:
|
|
104
|
+
completion(nil)
|
|
105
|
+
case .failure(let error):
|
|
106
|
+
completion("Screen Time authorization request failed: \(error.localizedDescription)")
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
private static func authorizationStatusString() -> String {
|
|
114
|
+
runOnMain {
|
|
115
|
+
switch AuthorizationCenter.shared.authorizationStatus {
|
|
116
|
+
case .approved:
|
|
117
|
+
return "approved"
|
|
118
|
+
case .denied:
|
|
119
|
+
return "denied"
|
|
120
|
+
case .notDetermined:
|
|
121
|
+
return "not-determined"
|
|
122
|
+
@unknown default:
|
|
123
|
+
return "unavailable"
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
private static func canRequestAuthorization(
|
|
129
|
+
familyControlsEnabled: Bool,
|
|
130
|
+
authorizationStatus: String
|
|
131
|
+
) -> Bool {
|
|
132
|
+
familyControlsEnabled && authorizationStatus != "approved"
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
private static func derivedReason(
|
|
136
|
+
familyControlsEnabled: Bool,
|
|
137
|
+
authorizationStatus: String
|
|
138
|
+
) -> String {
|
|
139
|
+
if !familyControlsEnabled {
|
|
140
|
+
return "Family Controls entitlement is missing from the app bundle."
|
|
141
|
+
}
|
|
142
|
+
if authorizationStatus == "not-determined" {
|
|
143
|
+
return "Screen Time authorization has not been granted yet."
|
|
144
|
+
}
|
|
145
|
+
if authorizationStatus == "denied" {
|
|
146
|
+
return "Screen Time authorization was denied on this device."
|
|
147
|
+
}
|
|
148
|
+
return "DeviceActivity report and monitor extensions are not wired in this checkout."
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
private static func inspectEntitlements() -> EntitlementInspection {
|
|
152
|
+
#if os(macOS)
|
|
153
|
+
return EntitlementInspection(
|
|
154
|
+
familyControls: entitlementIsEnabled(familyControlsEntitlement),
|
|
155
|
+
inspected: "code-signature",
|
|
156
|
+
reason: nil
|
|
157
|
+
)
|
|
158
|
+
#else
|
|
159
|
+
return EntitlementInspection(
|
|
160
|
+
familyControls: false,
|
|
161
|
+
inspected: "not-inspectable",
|
|
162
|
+
reason: "iOS entitlement inspection is handled by build validation and provisioning profile checks."
|
|
163
|
+
)
|
|
164
|
+
#endif
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
#if os(macOS)
|
|
168
|
+
private static func entitlementIsEnabled(_ key: String) -> Bool {
|
|
169
|
+
guard let task = SecTaskCreateFromSelf(nil) else {
|
|
170
|
+
return false
|
|
171
|
+
}
|
|
172
|
+
guard let value = SecTaskCopyValueForEntitlement(task, key as CFString, nil) else {
|
|
173
|
+
return false
|
|
174
|
+
}
|
|
175
|
+
if let boolean = value as? Bool {
|
|
176
|
+
return boolean
|
|
177
|
+
}
|
|
178
|
+
return false
|
|
179
|
+
}
|
|
180
|
+
#endif
|
|
181
|
+
|
|
182
|
+
private static func runOnMain<T>(_ work: () -> T) -> T {
|
|
183
|
+
if Thread.isMainThread {
|
|
184
|
+
return work()
|
|
185
|
+
}
|
|
186
|
+
return DispatchQueue.main.sync(execute: work)
|
|
187
|
+
}
|
|
188
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@elizaos/capacitor-mobile-signals",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Bridges mobile wake, lock, battery, and protected-data state into LifeOps.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"mobile",
|
|
7
|
+
"wake",
|
|
8
|
+
"sleep",
|
|
9
|
+
"battery",
|
|
10
|
+
"biometric"
|
|
11
|
+
],
|
|
12
|
+
"main": "./dist/plugin.cjs.js",
|
|
13
|
+
"module": "./dist/esm/index.js",
|
|
14
|
+
"types": "./dist/esm/index.d.ts",
|
|
15
|
+
"exports": {
|
|
16
|
+
".": {
|
|
17
|
+
"types": "./dist/esm/index.d.ts",
|
|
18
|
+
"import": "./dist/esm/index.js",
|
|
19
|
+
"require": "./dist/plugin.cjs.js"
|
|
20
|
+
},
|
|
21
|
+
"./package.json": "./package.json"
|
|
22
|
+
},
|
|
23
|
+
"unpkg": "dist/plugin.js",
|
|
24
|
+
"files": [
|
|
25
|
+
"android/src/main/",
|
|
26
|
+
"android/build.gradle",
|
|
27
|
+
"dist/",
|
|
28
|
+
"ios/Sources/",
|
|
29
|
+
"scripts/validate-ios-screen-time.mjs",
|
|
30
|
+
"*.podspec"
|
|
31
|
+
],
|
|
32
|
+
"author": "elizaOS",
|
|
33
|
+
"license": "MIT",
|
|
34
|
+
"repository": {
|
|
35
|
+
"type": "git",
|
|
36
|
+
"url": "https://github.com/elizaOS/eliza"
|
|
37
|
+
},
|
|
38
|
+
"scripts": {
|
|
39
|
+
"build": "npm run clean && tsc && rollup -c rollup.config.mjs",
|
|
40
|
+
"clean": "rimraf ./dist",
|
|
41
|
+
"prepublishOnly": "npm run build",
|
|
42
|
+
"test": "vitest run",
|
|
43
|
+
"validate:ios-screen-time": "node scripts/validate-ios-screen-time.mjs",
|
|
44
|
+
"watch": "tsc --watch"
|
|
45
|
+
},
|
|
46
|
+
"devDependencies": {
|
|
47
|
+
"@capacitor/android": "^8.0.0",
|
|
48
|
+
"@capacitor/core": "^8.3.1",
|
|
49
|
+
"@capacitor/ios": "^8.0.0",
|
|
50
|
+
"@rollup/plugin-node-resolve": "^16.0.0",
|
|
51
|
+
"rimraf": "^6.0.0",
|
|
52
|
+
"rollup": "^4.60.2",
|
|
53
|
+
"typescript": "^6.0.0",
|
|
54
|
+
"vitest": "^4.0.17"
|
|
55
|
+
},
|
|
56
|
+
"peerDependencies": {
|
|
57
|
+
"@capacitor/core": "^8.3.1"
|
|
58
|
+
},
|
|
59
|
+
"publishConfig": {
|
|
60
|
+
"access": "public"
|
|
61
|
+
},
|
|
62
|
+
"capacitor": {
|
|
63
|
+
"ios": {
|
|
64
|
+
"src": "ios",
|
|
65
|
+
"podName": "ElizaosCapacitorMobileSignals"
|
|
66
|
+
},
|
|
67
|
+
"android": {
|
|
68
|
+
"src": "android"
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
"elizaos": {
|
|
72
|
+
"platforms": [
|
|
73
|
+
"browser",
|
|
74
|
+
"node"
|
|
75
|
+
],
|
|
76
|
+
"runtime": "both",
|
|
77
|
+
"platformDetails": {
|
|
78
|
+
"browser": "Graceful web fallback from document visibility, focus, and Battery Status APIs.",
|
|
79
|
+
"node": "No desktop native integration; mobile signals are captured on iOS and Android only.",
|
|
80
|
+
"ios": true,
|
|
81
|
+
"android": true
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import process from "node:process";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
|
|
8
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
const pluginRoot = path.resolve(__dirname, "..");
|
|
10
|
+
const defaultRepoRoot = path.resolve(pluginRoot, "..", "..", "..");
|
|
11
|
+
|
|
12
|
+
export const IOS_SCREEN_TIME_REQUIREMENTS = Object.freeze({
|
|
13
|
+
entitlements: Object.freeze({
|
|
14
|
+
familyControls: "com.apple.developer.family-controls",
|
|
15
|
+
}),
|
|
16
|
+
frameworks: Object.freeze(["FamilyControls", "DeviceActivity"]),
|
|
17
|
+
appEntitlementsRelativePath: path.join("App", "App.entitlements"),
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
export function defaultIosScreenTimeValidationPaths({
|
|
21
|
+
repoRootValue = defaultRepoRoot,
|
|
22
|
+
} = {}) {
|
|
23
|
+
return {
|
|
24
|
+
entitlementsPath: path.join(
|
|
25
|
+
repoRootValue,
|
|
26
|
+
"packages",
|
|
27
|
+
"app-core",
|
|
28
|
+
"platforms",
|
|
29
|
+
"ios",
|
|
30
|
+
"App",
|
|
31
|
+
"App",
|
|
32
|
+
"App.entitlements",
|
|
33
|
+
),
|
|
34
|
+
projectPath: path.join(
|
|
35
|
+
repoRootValue,
|
|
36
|
+
"packages",
|
|
37
|
+
"app-core",
|
|
38
|
+
"platforms",
|
|
39
|
+
"ios",
|
|
40
|
+
"App",
|
|
41
|
+
"App.xcodeproj",
|
|
42
|
+
"project.pbxproj",
|
|
43
|
+
),
|
|
44
|
+
podspecPath: path.join(
|
|
45
|
+
repoRootValue,
|
|
46
|
+
"packages",
|
|
47
|
+
"native-plugins",
|
|
48
|
+
"mobile-signals",
|
|
49
|
+
"ElizaosCapacitorMobileSignals.podspec",
|
|
50
|
+
),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function escapeRegExp(value) {
|
|
55
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function addCheck(checks, id, ok, message, skipped = false) {
|
|
59
|
+
checks.push({ id, ok, skipped, message });
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function readRequiredText(filePath, label) {
|
|
63
|
+
if (!filePath || !fs.existsSync(filePath)) {
|
|
64
|
+
throw new Error(`${label} does not exist at ${filePath ?? "(not set)"}`);
|
|
65
|
+
}
|
|
66
|
+
return fs.readFileSync(filePath, "utf8");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function findKeyEnd(plist, key) {
|
|
70
|
+
const pattern = new RegExp(`<key>\\s*${escapeRegExp(key)}\\s*</key>`, "m");
|
|
71
|
+
const match = pattern.exec(plist);
|
|
72
|
+
return match ? match.index + match[0].length : -1;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function extractNextDict(plist, startIndex) {
|
|
76
|
+
const dictStart = plist.indexOf("<dict>", startIndex);
|
|
77
|
+
if (dictStart === -1) return null;
|
|
78
|
+
|
|
79
|
+
const tokenPattern = /<\/?dict>/g;
|
|
80
|
+
tokenPattern.lastIndex = dictStart;
|
|
81
|
+
let depth = 0;
|
|
82
|
+
for (;;) {
|
|
83
|
+
const match = tokenPattern.exec(plist);
|
|
84
|
+
if (!match) return null;
|
|
85
|
+
depth += match[0] === "<dict>" ? 1 : -1;
|
|
86
|
+
if (depth === 0) {
|
|
87
|
+
return plist.slice(dictStart, tokenPattern.lastIndex);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function extractDictAfterKey(plist, key) {
|
|
93
|
+
const keyEnd = findKeyEnd(plist, key);
|
|
94
|
+
if (keyEnd === -1) return null;
|
|
95
|
+
return extractNextDict(plist, keyEnd);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function plistBooleanIsTrue(plist, key, { enclosingKey } = {}) {
|
|
99
|
+
const source = enclosingKey
|
|
100
|
+
? extractDictAfterKey(plist, enclosingKey)
|
|
101
|
+
: plist;
|
|
102
|
+
if (!source) return false;
|
|
103
|
+
const pattern = new RegExp(
|
|
104
|
+
`<key>\\s*${escapeRegExp(key)}\\s*</key>\\s*<true\\s*/>`,
|
|
105
|
+
"m",
|
|
106
|
+
);
|
|
107
|
+
return pattern.test(source);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function missingRequiredEntitlements(plist, { enclosingKey } = {}) {
|
|
111
|
+
return Object.values(IOS_SCREEN_TIME_REQUIREMENTS.entitlements).filter(
|
|
112
|
+
(key) => !plistBooleanIsTrue(plist, key, { enclosingKey }),
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function decodeProvisioningProfile(profilePath) {
|
|
117
|
+
const raw = fs.readFileSync(profilePath);
|
|
118
|
+
const text = raw.toString("utf8");
|
|
119
|
+
if (text.includes("<plist")) {
|
|
120
|
+
return text;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (process.platform !== "darwin") {
|
|
124
|
+
throw new Error(
|
|
125
|
+
"binary provisioning profiles can only be decoded on macOS with the security tool",
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const result = spawnSync("security", ["cms", "-D", "-i", profilePath], {
|
|
130
|
+
encoding: "utf8",
|
|
131
|
+
});
|
|
132
|
+
if (result.status !== 0) {
|
|
133
|
+
throw new Error(
|
|
134
|
+
result.stderr.trim() ||
|
|
135
|
+
`security cms failed with exit code ${result.status ?? 1}`,
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
return result.stdout;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function validateIosScreenTimeBuildWiring(options = {}) {
|
|
142
|
+
const defaults = defaultIosScreenTimeValidationPaths(options);
|
|
143
|
+
const entitlementsPath =
|
|
144
|
+
options.entitlementsPath ?? defaults.entitlementsPath;
|
|
145
|
+
const projectPath = options.projectPath ?? defaults.projectPath;
|
|
146
|
+
const podspecPath = options.podspecPath ?? defaults.podspecPath;
|
|
147
|
+
const provisioningProfilePath =
|
|
148
|
+
options.provisioningProfilePath ??
|
|
149
|
+
process.env.MOBILE_SIGNALS_IOS_PROVISIONING_PROFILE;
|
|
150
|
+
const requireProvisioningProfile =
|
|
151
|
+
options.requireProvisioningProfile ??
|
|
152
|
+
process.env.MOBILE_SIGNALS_REQUIRE_IOS_PROVISIONING_PROFILE === "1";
|
|
153
|
+
const checks = [];
|
|
154
|
+
|
|
155
|
+
try {
|
|
156
|
+
const entitlements = readRequiredText(entitlementsPath, "iOS entitlements");
|
|
157
|
+
const missing = missingRequiredEntitlements(entitlements);
|
|
158
|
+
addCheck(
|
|
159
|
+
checks,
|
|
160
|
+
"app-entitlements",
|
|
161
|
+
missing.length === 0,
|
|
162
|
+
missing.length === 0
|
|
163
|
+
? `App entitlements include ${Object.values(
|
|
164
|
+
IOS_SCREEN_TIME_REQUIREMENTS.entitlements,
|
|
165
|
+
).join(", ")}.`
|
|
166
|
+
: `App entitlements are missing required Screen Time keys: ${missing.join(
|
|
167
|
+
", ",
|
|
168
|
+
)}.`,
|
|
169
|
+
);
|
|
170
|
+
} catch (error) {
|
|
171
|
+
addCheck(checks, "app-entitlements", false, error.message);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
try {
|
|
175
|
+
const project = readRequiredText(projectPath, "Xcode project");
|
|
176
|
+
const expected = `CODE_SIGN_ENTITLEMENTS = ${IOS_SCREEN_TIME_REQUIREMENTS.appEntitlementsRelativePath};`;
|
|
177
|
+
addCheck(
|
|
178
|
+
checks,
|
|
179
|
+
"xcode-entitlements-build-setting",
|
|
180
|
+
project.includes(expected),
|
|
181
|
+
project.includes(expected)
|
|
182
|
+
? "Xcode project signs the app target with App/App.entitlements."
|
|
183
|
+
: `Xcode project does not contain ${expected}`,
|
|
184
|
+
);
|
|
185
|
+
} catch (error) {
|
|
186
|
+
addCheck(checks, "xcode-entitlements-build-setting", false, error.message);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
try {
|
|
190
|
+
const podspec = readRequiredText(podspecPath, "mobile-signals podspec");
|
|
191
|
+
const missingFrameworks = IOS_SCREEN_TIME_REQUIREMENTS.frameworks.filter(
|
|
192
|
+
(framework) => !podspec.includes(framework),
|
|
193
|
+
);
|
|
194
|
+
addCheck(
|
|
195
|
+
checks,
|
|
196
|
+
"podspec-frameworks",
|
|
197
|
+
missingFrameworks.length === 0,
|
|
198
|
+
missingFrameworks.length === 0
|
|
199
|
+
? "mobile-signals podspec links FamilyControls and DeviceActivity."
|
|
200
|
+
: `mobile-signals podspec is missing frameworks: ${missingFrameworks.join(
|
|
201
|
+
", ",
|
|
202
|
+
)}.`,
|
|
203
|
+
);
|
|
204
|
+
} catch (error) {
|
|
205
|
+
addCheck(checks, "podspec-frameworks", false, error.message);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (provisioningProfilePath) {
|
|
209
|
+
try {
|
|
210
|
+
if (!fs.existsSync(provisioningProfilePath)) {
|
|
211
|
+
throw new Error(
|
|
212
|
+
`Provisioning profile does not exist at ${provisioningProfilePath}`,
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
const profilePlist = decodeProvisioningProfile(provisioningProfilePath);
|
|
216
|
+
const missing = missingRequiredEntitlements(profilePlist, {
|
|
217
|
+
enclosingKey: "Entitlements",
|
|
218
|
+
});
|
|
219
|
+
addCheck(
|
|
220
|
+
checks,
|
|
221
|
+
"provisioning-entitlements",
|
|
222
|
+
missing.length === 0,
|
|
223
|
+
missing.length === 0
|
|
224
|
+
? "Provisioning profile includes required Screen Time entitlements."
|
|
225
|
+
: `Provisioning profile is missing required Screen Time keys: ${missing.join(
|
|
226
|
+
", ",
|
|
227
|
+
)}.`,
|
|
228
|
+
);
|
|
229
|
+
} catch (error) {
|
|
230
|
+
addCheck(checks, "provisioning-entitlements", false, error.message);
|
|
231
|
+
}
|
|
232
|
+
} else if (requireProvisioningProfile) {
|
|
233
|
+
addCheck(
|
|
234
|
+
checks,
|
|
235
|
+
"provisioning-entitlements",
|
|
236
|
+
false,
|
|
237
|
+
"MOBILE_SIGNALS_REQUIRE_IOS_PROVISIONING_PROFILE=1 but no provisioning profile was supplied.",
|
|
238
|
+
);
|
|
239
|
+
} else {
|
|
240
|
+
addCheck(
|
|
241
|
+
checks,
|
|
242
|
+
"provisioning-entitlements",
|
|
243
|
+
true,
|
|
244
|
+
"No provisioning profile supplied; skipping profile entitlement inspection.",
|
|
245
|
+
true,
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const failures = checks.filter((check) => !check.ok);
|
|
250
|
+
return {
|
|
251
|
+
ok: failures.length === 0,
|
|
252
|
+
checks,
|
|
253
|
+
failures,
|
|
254
|
+
requirements: IOS_SCREEN_TIME_REQUIREMENTS,
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
export function assertIosScreenTimeBuildWiring(options = {}) {
|
|
259
|
+
const result = validateIosScreenTimeBuildWiring(options);
|
|
260
|
+
if (!result.ok) {
|
|
261
|
+
throw new Error(
|
|
262
|
+
[
|
|
263
|
+
"iOS Screen Time build wiring is invalid:",
|
|
264
|
+
...result.failures.map((failure) => `- ${failure.message}`),
|
|
265
|
+
].join("\n"),
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
return result;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function parseArgs(argv) {
|
|
272
|
+
const options = {};
|
|
273
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
274
|
+
const arg = argv[index];
|
|
275
|
+
const next = () => argv[++index];
|
|
276
|
+
if (arg === "--json") {
|
|
277
|
+
options.json = true;
|
|
278
|
+
} else if (arg === "--repo-root") {
|
|
279
|
+
options.repoRootValue = path.resolve(next());
|
|
280
|
+
} else if (arg === "--entitlements") {
|
|
281
|
+
options.entitlementsPath = path.resolve(next());
|
|
282
|
+
} else if (arg === "--project") {
|
|
283
|
+
options.projectPath = path.resolve(next());
|
|
284
|
+
} else if (arg === "--podspec") {
|
|
285
|
+
options.podspecPath = path.resolve(next());
|
|
286
|
+
} else if (arg === "--provisioning-profile") {
|
|
287
|
+
options.provisioningProfilePath = path.resolve(next());
|
|
288
|
+
} else if (arg === "--require-provisioning-profile") {
|
|
289
|
+
options.requireProvisioningProfile = true;
|
|
290
|
+
} else {
|
|
291
|
+
throw new Error(`Unknown argument: ${arg}`);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
return options;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const isMain =
|
|
298
|
+
process.argv[1] &&
|
|
299
|
+
path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
300
|
+
|
|
301
|
+
if (isMain) {
|
|
302
|
+
try {
|
|
303
|
+
const options = parseArgs(process.argv.slice(2));
|
|
304
|
+
const result = validateIosScreenTimeBuildWiring(options);
|
|
305
|
+
if (options.json) {
|
|
306
|
+
console.log(JSON.stringify(result, null, 2));
|
|
307
|
+
} else {
|
|
308
|
+
for (const check of result.checks) {
|
|
309
|
+
const prefix = check.skipped ? "SKIP" : check.ok ? "OK" : "FAIL";
|
|
310
|
+
console.log(`[${prefix}] ${check.message}`);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
if (!result.ok) {
|
|
314
|
+
process.exitCode = 1;
|
|
315
|
+
}
|
|
316
|
+
} catch (error) {
|
|
317
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
318
|
+
process.exitCode = 1;
|
|
319
|
+
}
|
|
320
|
+
}
|