erne-universal 0.10.8 → 0.10.10
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/lib/audit.js +31 -6
- package/lib/generate.js +34 -3
- package/package.json +1 -1
package/lib/audit.js
CHANGED
|
@@ -869,6 +869,31 @@ function checkPermissions(cwd, deps, findings, strengths) {
|
|
|
869
869
|
const declaredIosKeys = new Set(Object.keys(infoPlist));
|
|
870
870
|
const declaredAndroidPerms = new Set(androidPermissions);
|
|
871
871
|
|
|
872
|
+
// Expo plugins automatically add permissions at build time
|
|
873
|
+
// Map plugin names to the permissions they auto-configure
|
|
874
|
+
const plugins = expo?.plugins || [];
|
|
875
|
+
const pluginNames = new Set(plugins.map(p => Array.isArray(p) ? p[0] : p));
|
|
876
|
+
const PLUGIN_PERMISSIONS = {
|
|
877
|
+
'expo-camera': { iosKeys: ['NSCameraUsageDescription', 'NSMicrophoneUsageDescription'], androidPerms: ['android.permission.CAMERA'] },
|
|
878
|
+
'expo-location': { iosKeys: ['NSLocationWhenInUseUsageDescription', 'NSLocationAlwaysUsageDescription', 'NSLocationAlwaysAndWhenInUseUsageDescription'], androidPerms: ['android.permission.ACCESS_FINE_LOCATION', 'android.permission.ACCESS_COARSE_LOCATION'] },
|
|
879
|
+
'expo-av': { iosKeys: ['NSMicrophoneUsageDescription'], androidPerms: ['android.permission.RECORD_AUDIO'] },
|
|
880
|
+
'expo-audio': { iosKeys: ['NSMicrophoneUsageDescription'], androidPerms: ['android.permission.RECORD_AUDIO'] },
|
|
881
|
+
'expo-media-library': { iosKeys: ['NSPhotoLibraryUsageDescription', 'NSPhotoLibraryAddUsageDescription'], androidPerms: ['android.permission.READ_MEDIA_IMAGES'] },
|
|
882
|
+
'expo-image-picker': { iosKeys: ['NSCameraUsageDescription', 'NSPhotoLibraryUsageDescription'], androidPerms: ['android.permission.CAMERA'] },
|
|
883
|
+
'expo-contacts': { iosKeys: ['NSContactsUsageDescription'], androidPerms: ['android.permission.READ_CONTACTS'] },
|
|
884
|
+
'expo-calendar': { iosKeys: ['NSCalendarsUsageDescription'], androidPerms: ['android.permission.READ_CALENDAR'] },
|
|
885
|
+
'expo-notifications': { iosKeys: [], androidPerms: [] },
|
|
886
|
+
};
|
|
887
|
+
|
|
888
|
+
// Add plugin-provided permissions to declared sets
|
|
889
|
+
for (const pluginName of pluginNames) {
|
|
890
|
+
const pluginPerms = PLUGIN_PERMISSIONS[pluginName];
|
|
891
|
+
if (pluginPerms) {
|
|
892
|
+
for (const key of pluginPerms.iosKeys) declaredIosKeys.add(key);
|
|
893
|
+
for (const perm of pluginPerms.androidPerms) declaredAndroidPerms.add(perm);
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
|
|
872
897
|
let missingCount = 0;
|
|
873
898
|
let installedPermCount = 0;
|
|
874
899
|
const usedIosKeys = new Set();
|
|
@@ -884,27 +909,27 @@ function checkPermissions(cwd, deps, findings, strengths) {
|
|
|
884
909
|
if (api.iosKey) usedIosKeys.add(api.iosKey);
|
|
885
910
|
if (api.androidPerm) usedAndroidPerms.add(api.androidPerm);
|
|
886
911
|
|
|
887
|
-
// Check iOS plist declaration
|
|
912
|
+
// Check iOS plist declaration (manual infoPlist OR plugin-provided)
|
|
888
913
|
if (api.iosKey && appConfig && !declaredIosKeys.has(api.iosKey)) {
|
|
889
914
|
missingCount++;
|
|
890
915
|
findings.push({
|
|
891
916
|
severity: SEVERITY.critical,
|
|
892
917
|
category: CATEGORY.permissions,
|
|
893
918
|
title: `Missing iOS permission: ${api.iosKey}`,
|
|
894
|
-
detail: `${installed.join(', ')} requires ${name} permission but ${api.iosKey} is not declared in app.json ios.infoPlist.`,
|
|
895
|
-
fix: `Add "${api.iosKey}" to app.json > expo > ios > infoPlist`,
|
|
919
|
+
detail: `${installed.join(', ')} requires ${name} permission but ${api.iosKey} is not declared in app.json ios.infoPlist and no matching Expo plugin found.`,
|
|
920
|
+
fix: `Add "${api.iosKey}" to app.json > expo > ios > infoPlist, or add the corresponding Expo plugin`,
|
|
896
921
|
});
|
|
897
922
|
}
|
|
898
923
|
|
|
899
|
-
// Check Android permission declaration
|
|
924
|
+
// Check Android permission declaration (manual OR plugin-provided)
|
|
900
925
|
if (api.androidPerm && appConfig && androidPermissions.length > 0 && !declaredAndroidPerms.has(api.androidPerm)) {
|
|
901
926
|
missingCount++;
|
|
902
927
|
findings.push({
|
|
903
928
|
severity: SEVERITY.critical,
|
|
904
929
|
category: CATEGORY.permissions,
|
|
905
930
|
title: `Missing Android permission: ${api.androidPerm}`,
|
|
906
|
-
detail: `${installed.join(', ')} requires ${name} permission but ${api.androidPerm} is not declared in app.json android.permissions.`,
|
|
907
|
-
fix: `Add "${api.androidPerm}" to app.json > expo > android > permissions`,
|
|
931
|
+
detail: `${installed.join(', ')} requires ${name} permission but ${api.androidPerm} is not declared in app.json android.permissions and no matching Expo plugin found.`,
|
|
932
|
+
fix: `Add "${api.androidPerm}" to app.json > expo > android > permissions, or add the corresponding Expo plugin`,
|
|
908
933
|
});
|
|
909
934
|
}
|
|
910
935
|
}
|
package/lib/generate.js
CHANGED
|
@@ -333,20 +333,51 @@ function generateConfig(erneRoot, targetDir, detection, profile, mcpSelections)
|
|
|
333
333
|
fs.writeFileSync(destHooksPath, JSON.stringify(merged, null, 2));
|
|
334
334
|
}
|
|
335
335
|
|
|
336
|
-
// 7. Copy MCP configs
|
|
336
|
+
// 7. Copy MCP configs + generate .mcp.json for Claude Code
|
|
337
337
|
if (mcpSelections && mcpSelections.length > 0) {
|
|
338
338
|
const mcpSrc = path.join(erneRoot, 'mcp-configs');
|
|
339
339
|
const mcpDest = path.join(targetDir, 'mcp');
|
|
340
|
+
const mcpServers = {};
|
|
340
341
|
if (fs.existsSync(mcpSrc)) {
|
|
341
342
|
fs.mkdirSync(mcpDest, { recursive: true });
|
|
342
343
|
for (const sel of mcpSelections) {
|
|
343
|
-
|
|
344
|
+
// Try with and without .json extension
|
|
345
|
+
const srcFile = fs.existsSync(path.join(mcpSrc, sel + '.json'))
|
|
346
|
+
? path.join(mcpSrc, sel + '.json')
|
|
347
|
+
: path.join(mcpSrc, sel);
|
|
344
348
|
if (fs.existsSync(srcFile)) {
|
|
345
|
-
fs.copyFileSync(srcFile, path.join(mcpDest, sel));
|
|
349
|
+
fs.copyFileSync(srcFile, path.join(mcpDest, sel + '.json'));
|
|
346
350
|
mcpCount++;
|
|
351
|
+
// Parse config for .mcp.json generation
|
|
352
|
+
try {
|
|
353
|
+
const mcpConfig = JSON.parse(fs.readFileSync(srcFile, 'utf8'));
|
|
354
|
+
const serverEntry = {};
|
|
355
|
+
if (mcpConfig.command) serverEntry.command = mcpConfig.command;
|
|
356
|
+
if (mcpConfig.args) serverEntry.args = mcpConfig.args;
|
|
357
|
+
if (mcpConfig.env) serverEntry.env = mcpConfig.env;
|
|
358
|
+
if (mcpConfig.url) serverEntry.url = mcpConfig.url;
|
|
359
|
+
mcpServers[sel] = serverEntry;
|
|
360
|
+
} catch { /* skip invalid json */ }
|
|
347
361
|
}
|
|
348
362
|
}
|
|
349
363
|
}
|
|
364
|
+
// Write .mcp.json at project root for Claude Code
|
|
365
|
+
if (Object.keys(mcpServers).length > 0) {
|
|
366
|
+
const projectRoot = path.dirname(targetDir); // targetDir is .claude/, project root is parent
|
|
367
|
+
const mcpJsonPath = path.join(projectRoot, '.mcp.json');
|
|
368
|
+
let existingMcp = {};
|
|
369
|
+
if (fs.existsSync(mcpJsonPath)) {
|
|
370
|
+
try { existingMcp = JSON.parse(fs.readFileSync(mcpJsonPath, 'utf8')); } catch { /* fresh */ }
|
|
371
|
+
}
|
|
372
|
+
if (!existingMcp.mcpServers) existingMcp.mcpServers = {};
|
|
373
|
+
// Merge — don't overwrite existing servers
|
|
374
|
+
for (const [name, config] of Object.entries(mcpServers)) {
|
|
375
|
+
if (!existingMcp.mcpServers[name]) {
|
|
376
|
+
existingMcp.mcpServers[name] = config;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
fs.writeFileSync(mcpJsonPath, JSON.stringify(existingMcp, null, 2) + '\n');
|
|
380
|
+
}
|
|
350
381
|
}
|
|
351
382
|
|
|
352
383
|
// 8. Write settings.json with full detection profile
|