tadforce 0.0.0-stage → 0.3.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/.claude-plugin/marketplace.json +20 -0
- package/.claude-plugin/plugin.json +13 -0
- package/LICENSE +202 -0
- package/NOTICE +11 -0
- package/README.md +266 -2
- package/assets/logo-mark.svg +24 -0
- package/assets/logo.svg +31 -0
- package/bin/tadforce.js +4 -0
- package/commands/tadforce-review.md +8 -0
- package/commands/tadforce-update.md +6 -0
- package/commands/tadforce.md +10 -0
- package/package.json +60 -4
- package/skills/tadforce/SKILL.md +73 -0
- package/skills/tadforce/references/axes-rules.md +29 -0
- package/skills/tadforce/references/evidence-rules.md +40 -0
- package/skills/tadforce/references/exclusion-heuristics.md +40 -0
- package/skills/tadforce/references/figure-rules.md +42 -0
- package/skills/tadforce/references/markdown-conventions.md +67 -0
- package/skills/tadforce/references/modules/agentforce-ai.md +24 -0
- package/skills/tadforce/references/modules/analytics.md +19 -0
- package/skills/tadforce/references/modules/commerce.md +25 -0
- package/skills/tadforce/references/modules/data-cloud.md +23 -0
- package/skills/tadforce/references/modules/experience-cloud.md +24 -0
- package/skills/tadforce/references/modules/field-service.md +25 -0
- package/skills/tadforce/references/modules/industries.md +21 -0
- package/skills/tadforce/references/modules/integration-platforms.md +19 -0
- package/skills/tadforce/references/modules/marketing.md +24 -0
- package/skills/tadforce/references/modules/omnistudio.md +24 -0
- package/skills/tadforce/references/modules/platform-events.md +19 -0
- package/skills/tadforce/references/modules/revenue-cpq.md +25 -0
- package/skills/tadforce/references/modules/sales-cloud.md +26 -0
- package/skills/tadforce/references/modules/security-shield.md +18 -0
- package/skills/tadforce/references/modules/service-cloud.md +27 -0
- package/skills/tadforce/references/naming-external-systems.md +32 -0
- package/skills/tadforce/references/questions.md +59 -0
- package/skills/tadforce/references/salesforce-skills.md +27 -0
- package/skills/tadforce/references/workflow.md +95 -0
- package/skills/tadforce/references/writing-rules.md +47 -0
- package/skills/tadforce-review/SKILL.md +53 -0
- package/skills/tadforce-update/SKILL.md +48 -0
- package/src/build/brand.js +107 -0
- package/src/build/browser.js +70 -0
- package/src/build/directives.js +293 -0
- package/src/build/export.js +60 -0
- package/src/build/figures.js +119 -0
- package/src/build/mermaid.js +107 -0
- package/src/build/render.js +299 -0
- package/src/build/serve.js +59 -0
- package/src/cli.js +68 -0
- package/src/commands/docs.js +204 -0
- package/src/commands/doctor.js +370 -0
- package/src/commands/init.js +132 -0
- package/src/commands/org.js +151 -0
- package/src/commands/review.js +35 -0
- package/src/commands/spec.js +91 -0
- package/src/commands/update.js +154 -0
- package/src/facts/extractors/apex.js +111 -0
- package/src/facts/extractors/automation.js +85 -0
- package/src/facts/extractors/data.js +94 -0
- package/src/facts/extractors/integration.js +76 -0
- package/src/facts/extractors/misc.js +100 -0
- package/src/facts/extractors/security.js +92 -0
- package/src/facts/extractors/ui.js +120 -0
- package/src/facts/fingerprints.js +32 -0
- package/src/facts/heuristics/exclusions.js +143 -0
- package/src/facts/heuristics/modules.js +85 -0
- package/src/facts/heuristics/naming.js +31 -0
- package/src/facts/heuristics/signals.js +76 -0
- package/src/facts/index.js +197 -0
- package/src/facts/org.js +87 -0
- package/src/facts/xml.js +85 -0
- package/src/sf/sf.js +120 -0
- package/src/util/args.js +21 -0
- package/src/util/config.js +104 -0
- package/src/util/fsx.js +58 -0
- package/src/util/i18n.js +49 -0
- package/src/util/log.js +31 -0
- package/src/util/paths.js +11 -0
- package/src/validate/index.js +126 -0
- package/templates/axes/en/00-document-control.md +3 -0
- package/templates/axes/en/01-summary.md +7 -0
- package/templates/axes/en/02-method.md +9 -0
- package/templates/axes/en/03-findings.md +19 -0
- package/templates/axes/en/04-register.md +3 -0
- package/templates/axes/en/05-roadmap.md +3 -0
- package/templates/axes/en/99-appendix.md +5 -0
- package/templates/axes/fr/00-document-control.md +3 -0
- package/templates/axes/fr/01-summary.md +7 -0
- package/templates/axes/fr/02-method.md +9 -0
- package/templates/axes/fr/03-findings.md +19 -0
- package/templates/axes/fr/04-register.md +3 -0
- package/templates/axes/fr/05-roadmap.md +3 -0
- package/templates/axes/fr/99-appendix.md +5 -0
- package/templates/html/cover.html +12 -0
- package/templates/html/editor.js +502 -0
- package/templates/html/style.css +255 -0
- package/templates/html/template.html +88 -0
- package/templates/i18n/en.json +366 -0
- package/templates/i18n/fr.json +366 -0
- package/templates/manifest/package-tad.xml +543 -0
- package/templates/specs/sections.yml +494 -0
- package/templates/tad/en/00-document-control.md +7 -0
- package/templates/tad/en/01-summary.md +17 -0
- package/templates/tad/en/02-purpose.md +35 -0
- package/templates/tad/en/03-context.md +17 -0
- package/templates/tad/en/04-general.md +29 -0
- package/templates/tad/en/05-functional.md +7 -0
- package/templates/tad/en/06-data.md +71 -0
- package/templates/tad/en/07-application.md +49 -0
- package/templates/tad/en/08-ui.md +41 -0
- package/templates/tad/en/09-integrations.md +53 -0
- package/templates/tad/en/10-security.md +59 -0
- package/templates/tad/en/11-reporting.md +13 -0
- package/templates/tad/en/12-operations.md +25 -0
- package/templates/tad/en/13-nfr.md +9 -0
- package/templates/tad/en/14-decisions.md +3 -0
- package/templates/tad/en/15-outlook.md +3 -0
- package/templates/tad/en/99-appendices.md +119 -0
- package/templates/tad/fr/00-document-control.md +7 -0
- package/templates/tad/fr/01-summary.md +17 -0
- package/templates/tad/fr/02-purpose.md +35 -0
- package/templates/tad/fr/03-context.md +17 -0
- package/templates/tad/fr/04-general.md +29 -0
- package/templates/tad/fr/05-functional.md +7 -0
- package/templates/tad/fr/06-data.md +71 -0
- package/templates/tad/fr/07-application.md +49 -0
- package/templates/tad/fr/08-ui.md +41 -0
- package/templates/tad/fr/09-integrations.md +53 -0
- package/templates/tad/fr/10-security.md +59 -0
- package/templates/tad/fr/11-reporting.md +13 -0
- package/templates/tad/fr/12-operations.md +25 -0
- package/templates/tad/fr/13-nfr.md +9 -0
- package/templates/tad/fr/14-decisions.md +3 -0
- package/templates/tad/fr/15-outlook.md +3 -0
- package/templates/tad/fr/99-appendices.md +119 -0
- package/templates/vendor/LICENSE-html-docx-js.txt +21 -0
- package/templates/vendor/LICENSE-html2canvas.txt +22 -0
- package/templates/vendor/LICENSE-mermaid.txt +21 -0
- package/templates/vendor/html-docx.js +13216 -0
- package/templates/vendor/html2canvas.min.js +20 -0
- package/templates/vendor/mermaid.min.js +2314 -0
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { files, baseName, safeRead, readXml, str } from '../xml.js';
|
|
2
|
+
|
|
3
|
+
/** Split Apex source into code without comments/strings, and the list of string literals. */
|
|
4
|
+
export function scanApex(src) {
|
|
5
|
+
let code = '', i = 0;
|
|
6
|
+
const strings = [];
|
|
7
|
+
while (i < src.length) {
|
|
8
|
+
const ch = src[i], nx = src[i + 1];
|
|
9
|
+
if (ch === '/' && nx === '*') { const e = src.indexOf('*/', i + 2); i = e < 0 ? src.length : e + 2; code += ' '; continue; }
|
|
10
|
+
if (ch === '/' && nx === '/') { const e = src.indexOf('\n', i); i = e < 0 ? src.length : e; continue; }
|
|
11
|
+
if (ch === "'") {
|
|
12
|
+
let j = i + 1, s = '';
|
|
13
|
+
while (j < src.length && src[j] !== "'") { if (src[j] === '\\') { s += src[j + 1] ?? ''; j += 2; } else s += src[j++]; }
|
|
14
|
+
strings.push(s); code += "''"; i = j + 1; continue;
|
|
15
|
+
}
|
|
16
|
+
code += ch; i++;
|
|
17
|
+
}
|
|
18
|
+
return { code, strings };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const KIND_RULES = [
|
|
22
|
+
['batch', /implements[^{]*\bDatabase\.Batchable\b/i],
|
|
23
|
+
['queueable', /implements[^{]*\bQueueable\b/i],
|
|
24
|
+
['schedulable', /implements[^{]*\bSchedulable\b/i],
|
|
25
|
+
['future', /@future\b/i],
|
|
26
|
+
['auraEnabled', /@AuraEnabled\b/i],
|
|
27
|
+
['invocable', /@InvocableMethod\b/i],
|
|
28
|
+
['soap', /\bwebservice\s+static\b/i],
|
|
29
|
+
['callout', /\bnew\s+Http\s*\(|\bHttpRequest\b/i],
|
|
30
|
+
['registrationHandler', /implements[^{]*\bAuth\.RegistrationHandler\b/i],
|
|
31
|
+
['emailHandler', /implements[^{]*\bMessaging\.InboundEmailHandler\b/i],
|
|
32
|
+
['stateful', /\bDatabase\.Stateful\b/i],
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
export function extractApex(roots) {
|
|
36
|
+
const classes = [];
|
|
37
|
+
for (const f of files(roots, 'classes', '.cls')) {
|
|
38
|
+
const name = baseName(f, '.cls');
|
|
39
|
+
const raw = safeRead(f);
|
|
40
|
+
const { code, strings } = scanApex(raw);
|
|
41
|
+
const meta = readXml(f + '-meta.xml');
|
|
42
|
+
const declIdx = code.search(/\b(class|interface|enum)\s+\w+/i);
|
|
43
|
+
const decl = declIdx >= 0 ? code.slice(0, declIdx + 60) : code.slice(0, 400);
|
|
44
|
+
const declKind = (/\b(class|interface|enum)\s+\w+/i.exec(decl) || [])[1]?.toLowerCase() || 'class';
|
|
45
|
+
const isTest = /@istest\b/i.test(code.slice(0, Math.max(0, declIdx))) || (/@istest\b/i.test(code) && /Test$/i.test(name) && declKind === 'class');
|
|
46
|
+
const sharing = (/\b(with|without|inherited)\s+sharing\b/i.exec(decl) || [])[1]?.toLowerCase() || (declKind === 'class' ? 'none' : 'n/a');
|
|
47
|
+
const noComments = raw.replace(/\/\*[\s\S]*?\*\//g, ' ').replace(/\/\/[^\n]*/g, '');
|
|
48
|
+
const rest = /@RestResource\s*\(\s*urlMapping\s*=\s*'([^']+)'/i.exec(noComments);
|
|
49
|
+
const kinds = KIND_RULES.filter(([, re]) => re.test(code)).map(([k]) => k);
|
|
50
|
+
if (rest) kinds.push('rest');
|
|
51
|
+
if (/\babstract\s+class\b/i.test(decl)) kinds.push('abstract');
|
|
52
|
+
if (declKind !== 'class') kinds.push(declKind);
|
|
53
|
+
if (/^global\b/i.test(decl.trim()) || /\bglobal\s+(with|without|inherited|abstract|virtual|class)/i.test(decl)) kinds.push('global');
|
|
54
|
+
const namedCredentials = [...new Set(strings.map((s) => /^callout:([A-Za-z0-9_]+)/.exec(s)?.[1]).filter(Boolean))].sort();
|
|
55
|
+
const hardcodedIds = strings.filter((s) => /^[a-zA-Z0-9]{15}([a-zA-Z0-9]{3})?$/.test(s) && /^0[0-9A-Za-z]{2}/.test(s) && /\d/.test(s) && /[A-Za-z]/.test(s.slice(3))).length;
|
|
56
|
+
classes.push({
|
|
57
|
+
name,
|
|
58
|
+
apiVersion: str(meta.apiVersion),
|
|
59
|
+
test: !!isTest,
|
|
60
|
+
sharing: isTest ? 'n/a' : sharing,
|
|
61
|
+
kinds: [...new Set(kinds)].sort(),
|
|
62
|
+
restUrl: rest ? rest[1] : null,
|
|
63
|
+
namedCredentials,
|
|
64
|
+
userMode: (code.match(/\bWITH\s+USER_MODE\b|AccessLevel\.USER_MODE/gi) || []).length,
|
|
65
|
+
securityEnforced: (code.match(/\bWITH\s+SECURITY_ENFORCED\b/gi) || []).length,
|
|
66
|
+
stripInaccessible: (code.match(/Security\.stripInaccessible/gi) || []).length,
|
|
67
|
+
hardcodedIds,
|
|
68
|
+
soqlInLoops: countSoqlInLoops(code),
|
|
69
|
+
lines: raw.split('\n').filter((l) => l.trim()).length,
|
|
70
|
+
_tokens: tokens(code),
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const triggers = [];
|
|
75
|
+
for (const f of files(roots, 'triggers', '.trigger')) {
|
|
76
|
+
const name = baseName(f, '.trigger');
|
|
77
|
+
const { code } = scanApex(safeRead(f));
|
|
78
|
+
const m = /\btrigger\s+(\w+)\s+on\s+(\w+)\s*\(([^)]*)\)/i.exec(code);
|
|
79
|
+
const meta = readXml(f + '-meta.xml');
|
|
80
|
+
triggers.push({
|
|
81
|
+
name,
|
|
82
|
+
object: m ? m[2] : null,
|
|
83
|
+
events: m ? m[3].split(',').map((e) => e.trim().toLowerCase().replace(/\s+/g, ' ')).filter(Boolean) : [],
|
|
84
|
+
apiVersion: str(meta.apiVersion),
|
|
85
|
+
active: str(meta.status) ? str(meta.status) === 'Active' : true,
|
|
86
|
+
lines: code.split('\n').filter((l) => l.trim()).length,
|
|
87
|
+
_tokens: tokens(code),
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
return { classes, triggers };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function tokens(code) {
|
|
94
|
+
return new Set(code.match(/[A-Za-z_][A-Za-z0-9_]*/g) || []);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Rough count of SOQL/DML statements placed inside for/while loops (review signal, not proof). */
|
|
98
|
+
function countSoqlInLoops(code) {
|
|
99
|
+
let n = 0;
|
|
100
|
+
const re = /\b(for|while)\s*\([^)]*\)\s*\{/gi;
|
|
101
|
+
let m;
|
|
102
|
+
while ((m = re.exec(code))) {
|
|
103
|
+
let depth = 1, j = m.index + m[0].length;
|
|
104
|
+
const start = j;
|
|
105
|
+
while (j < code.length && depth) { if (code[j] === '{') depth++; else if (code[j] === '}') depth--; j++; }
|
|
106
|
+
const body = code.slice(start, j);
|
|
107
|
+
// a `for (x : [SELECT ...])` loop header is fine; only statements inside the body count
|
|
108
|
+
n += (body.match(/\[\s*SELECT\b|\bDatabase\.query\s*\(|\b(insert|update|upsert|delete)\s+\w/gi) || []).length;
|
|
109
|
+
}
|
|
110
|
+
return n;
|
|
111
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { files, baseName, readXml, arr, bool, str, byName } from '../xml.js';
|
|
2
|
+
|
|
3
|
+
const desc = (v) => { const s = str(v); return s ? s.replace(/\s+/g, ' ').slice(0, 280) : null; };
|
|
4
|
+
|
|
5
|
+
export function extractAutomation(roots) {
|
|
6
|
+
const flowDefs = {};
|
|
7
|
+
for (const f of files(roots, 'flowDefinitions', '.flowDefinition-meta.xml')) {
|
|
8
|
+
const x = readXml(f);
|
|
9
|
+
flowDefs[baseName(f, '.flowDefinition-meta.xml')] = str(x.activeVersionNumber);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const flows = files(roots, 'flows', '.flow-meta.xml').map((f) => {
|
|
13
|
+
const x = readXml(f);
|
|
14
|
+
const name = baseName(f, '.flow-meta.xml');
|
|
15
|
+
const start = x.start || {};
|
|
16
|
+
const sched = start.schedule || {};
|
|
17
|
+
let status = str(x.status) || 'Unknown';
|
|
18
|
+
if (flowDefs[name] === '0') status = 'Inactive';
|
|
19
|
+
const actionCalls = arr(x.actionCalls);
|
|
20
|
+
return {
|
|
21
|
+
name,
|
|
22
|
+
label: str(x.label),
|
|
23
|
+
processType: str(x.processType),
|
|
24
|
+
status,
|
|
25
|
+
active: status === 'Active',
|
|
26
|
+
apiVersion: str(x.apiVersion),
|
|
27
|
+
object: str(start.object),
|
|
28
|
+
triggerType: str(start.triggerType),
|
|
29
|
+
recordTriggerType: str(start.recordTriggerType),
|
|
30
|
+
scheduleFrequency: str(sched.frequency),
|
|
31
|
+
description: desc(x.description),
|
|
32
|
+
screens: arr(x.screens).length,
|
|
33
|
+
subflows: [...new Set(arr(x.subflows).map((s) => str(s.flowName)).filter(Boolean))].sort(),
|
|
34
|
+
apexActions: [...new Set(actionCalls.filter((a) => str(a.actionType) === 'apex').map((a) => str(a.actionName)).filter(Boolean))].sort(),
|
|
35
|
+
recordOps: {
|
|
36
|
+
create: arr(x.recordCreates).length,
|
|
37
|
+
update: arr(x.recordUpdates).length,
|
|
38
|
+
lookup: arr(x.recordLookups).length,
|
|
39
|
+
delete: arr(x.recordDeletes).length,
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
}).sort(byName);
|
|
43
|
+
|
|
44
|
+
const omni = (folder, suffix, extra = () => ({})) => files(roots, folder, suffix).map((f) => {
|
|
45
|
+
const x = readXml(f);
|
|
46
|
+
return {
|
|
47
|
+
name: baseName(f, suffix),
|
|
48
|
+
label: str(x.name),
|
|
49
|
+
type: str(x.type),
|
|
50
|
+
subType: str(x.subType),
|
|
51
|
+
language: str(x.language),
|
|
52
|
+
active: bool(x.isActive),
|
|
53
|
+
version: str(x.versionNumber),
|
|
54
|
+
...extra(x),
|
|
55
|
+
};
|
|
56
|
+
}).sort(byName);
|
|
57
|
+
|
|
58
|
+
const omnistudio = {
|
|
59
|
+
omniScripts: omni('omniScripts', '.os-meta.xml'),
|
|
60
|
+
integrationProcedures: omni('omniIntegrationProcedures', '.oip-meta.xml'),
|
|
61
|
+
dataMappers: omni('omniDataTransforms', '.rpt-meta.xml', (x) => ({ inputType: str(x.inputType), outputType: str(x.outputType) })),
|
|
62
|
+
flexCards: omni('omniUiCard', '.ouc-meta.xml'),
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
// Legacy automation (review signals)
|
|
66
|
+
let workflowRules = 0, workflowFieldUpdates = 0, workflowAlerts = 0;
|
|
67
|
+
for (const f of files(roots, 'workflows', '.workflow-meta.xml')) {
|
|
68
|
+
const x = readXml(f);
|
|
69
|
+
workflowRules += arr(x.rules).length;
|
|
70
|
+
workflowFieldUpdates += arr(x.fieldUpdates).length;
|
|
71
|
+
workflowAlerts += arr(x.alerts).length;
|
|
72
|
+
}
|
|
73
|
+
const count = (folder, suffix) => files(roots, folder, suffix).length;
|
|
74
|
+
const legacy = {
|
|
75
|
+
workflowRules, workflowFieldUpdates, workflowAlerts,
|
|
76
|
+
processBuilders: flows.filter((f) => f.processType === 'Workflow').length,
|
|
77
|
+
approvalProcesses: count('approvalProcesses', '.approvalProcess-meta.xml'),
|
|
78
|
+
assignmentRules: count('assignmentRules', '.assignmentRules-meta.xml'),
|
|
79
|
+
autoResponseRules: count('autoResponseRules', '.autoResponseRules-meta.xml'),
|
|
80
|
+
escalationRules: count('escalationRules', '.escalationRules-meta.xml'),
|
|
81
|
+
duplicateRules: count('duplicateRules', '.duplicateRule-meta.xml'),
|
|
82
|
+
matchingRules: count('matchingRules', '.matchingRule-meta.xml'),
|
|
83
|
+
};
|
|
84
|
+
return { flows, omnistudio, rules: legacy };
|
|
85
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { bundles, files, baseName, readXml, arr, bool, str, byName } from '../xml.js';
|
|
3
|
+
|
|
4
|
+
const RELATION_TYPES = new Set(['Lookup', 'MasterDetail', 'Hierarchy', 'ExternalLookup', 'IndirectLookup', 'MetadataRelationship']);
|
|
5
|
+
|
|
6
|
+
export function objectKind(name, meta = {}) {
|
|
7
|
+
if (meta.customSettingsType) return 'customSetting';
|
|
8
|
+
if (name.endsWith('__mdt')) return 'customMetadataType';
|
|
9
|
+
if (name.endsWith('__e')) return 'platformEvent';
|
|
10
|
+
if (name.endsWith('__b')) return 'bigObject';
|
|
11
|
+
if (name.endsWith('__x')) return 'externalObject';
|
|
12
|
+
if (name.endsWith('__c')) return 'custom';
|
|
13
|
+
return 'standard';
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function namespaceOf(apiName) {
|
|
17
|
+
const m = /^([A-Za-z][A-Za-z0-9]{0,14})__[A-Za-z0-9_]+?__(c|mdt|e|b|x|r)$/.exec(apiName);
|
|
18
|
+
return m ? m[1] : null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const desc = (v) => { const s = str(v); return s ? s.replace(/\s+/g, ' ').slice(0, 280) : null; };
|
|
22
|
+
|
|
23
|
+
export function extractData(roots) {
|
|
24
|
+
const objects = [];
|
|
25
|
+
for (const b of bundles(roots, 'objects')) {
|
|
26
|
+
const meta = readXml(path.join(b.dir, `${b.name}.object-meta.xml`));
|
|
27
|
+
const kind = objectKind(b.name, meta);
|
|
28
|
+
const fields = files([b.dir], 'fields', '.field-meta.xml').map((f) => {
|
|
29
|
+
const x = readXml(f);
|
|
30
|
+
const name = baseName(f, '.field-meta.xml');
|
|
31
|
+
return {
|
|
32
|
+
name,
|
|
33
|
+
label: str(x.label),
|
|
34
|
+
type: str(x.type) || (x.formula ? 'Formula' : null),
|
|
35
|
+
referenceTo: arr(x.referenceTo).map(String),
|
|
36
|
+
relationshipName: str(x.relationshipName),
|
|
37
|
+
externalId: bool(x.externalId),
|
|
38
|
+
unique: bool(x.unique),
|
|
39
|
+
required: bool(x.required),
|
|
40
|
+
formula: x.formula != null,
|
|
41
|
+
namespace: namespaceOf(name),
|
|
42
|
+
custom: name.endsWith('__c'),
|
|
43
|
+
};
|
|
44
|
+
});
|
|
45
|
+
const recordTypes = files([b.dir], 'recordTypes', '.recordType-meta.xml').map((f) => {
|
|
46
|
+
const x = readXml(f);
|
|
47
|
+
return { name: baseName(f, '.recordType-meta.xml'), label: str(x.label), active: x.active == null ? true : bool(x.active) };
|
|
48
|
+
});
|
|
49
|
+
const validationRules = files([b.dir], 'validationRules', '.validationRule-meta.xml').map((f) => {
|
|
50
|
+
const x = readXml(f);
|
|
51
|
+
return { name: baseName(f, '.validationRule-meta.xml'), active: bool(x.active) };
|
|
52
|
+
});
|
|
53
|
+
objects.push({
|
|
54
|
+
name: b.name,
|
|
55
|
+
label: str(meta.label),
|
|
56
|
+
kind,
|
|
57
|
+
namespace: namespaceOf(b.name),
|
|
58
|
+
description: desc(meta.description),
|
|
59
|
+
sharingModel: str(meta.sharingModel),
|
|
60
|
+
externalSharingModel: str(meta.externalSharingModel),
|
|
61
|
+
historyTracking: bool(meta.enableHistory),
|
|
62
|
+
fieldsTotal: fields.length,
|
|
63
|
+
customFields: fields.filter((f) => f.custom && !f.namespace).length,
|
|
64
|
+
customFieldNames: fields.filter((f) => f.custom).map((f) => f.name).sort(),
|
|
65
|
+
externalIds: fields.filter((f) => f.externalId).map((f) => f.name).sort(),
|
|
66
|
+
relations: fields.filter((f) => RELATION_TYPES.has(f.type) && f.referenceTo.length).map((f) => ({ field: f.name, type: f.type, to: f.referenceTo, relationshipName: f.relationshipName }))
|
|
67
|
+
.sort((a, b) => a.field.localeCompare(b.field)),
|
|
68
|
+
formulaFields: fields.filter((f) => f.formula).length,
|
|
69
|
+
recordTypes: recordTypes.sort(byName),
|
|
70
|
+
validationRules: validationRules.sort(byName),
|
|
71
|
+
fieldSets: files([b.dir], 'fieldSets', '.fieldSet-meta.xml').length,
|
|
72
|
+
businessProcesses: files([b.dir], 'businessProcesses', '.businessProcess-meta.xml').length,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Custom Metadata records: <Type>.<Record>.md-meta.xml
|
|
77
|
+
const cmdt = {};
|
|
78
|
+
for (const f of files(roots, 'customMetadata', '.md-meta.xml')) {
|
|
79
|
+
const base = baseName(f, '.md-meta.xml');
|
|
80
|
+
const dot = base.indexOf('.');
|
|
81
|
+
if (dot < 0) continue;
|
|
82
|
+
const type = base.slice(0, dot) + '__mdt';
|
|
83
|
+
(cmdt[type] ||= []).push(base.slice(dot + 1));
|
|
84
|
+
}
|
|
85
|
+
const cmdtTypes = Object.keys(cmdt).sort().map((type) => ({ type, records: cmdt[type].length, names: cmdt[type].sort() }));
|
|
86
|
+
|
|
87
|
+
const globalValueSets = files(roots, 'globalValueSets', '.globalValueSet-meta.xml').map((f) => baseName(f, '.globalValueSet-meta.xml')).sort();
|
|
88
|
+
const cdc = files(roots, 'platformEventChannelMembers', '.platformEventChannelMember-meta.xml').map((f) => {
|
|
89
|
+
const x = readXml(f);
|
|
90
|
+
return { name: baseName(f, '.platformEventChannelMember-meta.xml'), entity: str(x.selectedEntity), channel: str(x.eventChannel) };
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
return { objects: objects.sort(byName), cmdtTypes, globalValueSets, cdc: cdc.sort(byName) };
|
|
94
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { files, baseName, readXml, arr, bool, str, byName, host } from '../xml.js';
|
|
2
|
+
|
|
3
|
+
export function extractIntegration(roots) {
|
|
4
|
+
const namedCredentials = files(roots, 'namedCredentials', '.namedCredential-meta.xml').map((f) => {
|
|
5
|
+
const x = readXml(f);
|
|
6
|
+
const params = arr(x.namedCredentialParameters);
|
|
7
|
+
const url = str(x.endpoint) || str((params.find((p) => str(p.parameterType) === 'Url') || {}).parameterValue);
|
|
8
|
+
const ec = str((params.find((p) => str(p.parameterType) === 'Authentication') || {}).externalCredential);
|
|
9
|
+
return {
|
|
10
|
+
name: baseName(f, '.namedCredential-meta.xml'),
|
|
11
|
+
label: str(x.label),
|
|
12
|
+
type: str(x.namedCredentialType) || 'Legacy',
|
|
13
|
+
host: host(url),
|
|
14
|
+
externalCredential: ec,
|
|
15
|
+
protocol: str(x.protocol),
|
|
16
|
+
principalType: str(x.principalType),
|
|
17
|
+
generateAuthorizationHeader: x.generateAuthorizationHeader == null ? null : bool(x.generateAuthorizationHeader),
|
|
18
|
+
};
|
|
19
|
+
}).sort(byName);
|
|
20
|
+
|
|
21
|
+
const externalCredentials = files(roots, 'externalCredentials', '.externalCredential-meta.xml').map((f) => {
|
|
22
|
+
const x = readXml(f);
|
|
23
|
+
return { name: baseName(f, '.externalCredential-meta.xml'), label: str(x.label), protocol: str(x.authenticationProtocol), principals: arr(x.externalCredentialParameters).filter((p) => /Principal/.test(str(p.parameterType) || '')).length };
|
|
24
|
+
}).sort(byName);
|
|
25
|
+
|
|
26
|
+
const remoteSites = files(roots, 'remoteSiteSettings', '.remoteSite-meta.xml').map((f) => {
|
|
27
|
+
const x = readXml(f);
|
|
28
|
+
return { name: baseName(f, '.remoteSite-meta.xml'), host: host(str(x.url)), active: x.isActive == null ? true : bool(x.isActive), insecure: bool(x.disableProtocolSecurity) };
|
|
29
|
+
}).sort(byName);
|
|
30
|
+
|
|
31
|
+
const connectedApps = files(roots, 'connectedApps', '.connectedApp-meta.xml').map((f) => {
|
|
32
|
+
const x = readXml(f);
|
|
33
|
+
const oauth = x.oauthConfig || {};
|
|
34
|
+
return {
|
|
35
|
+
name: baseName(f, '.connectedApp-meta.xml'),
|
|
36
|
+
label: str(x.label),
|
|
37
|
+
oauth: !!x.oauthConfig,
|
|
38
|
+
scopes: arr(oauth.scopes).map(String).sort(),
|
|
39
|
+
saml: !!x.samlConfig,
|
|
40
|
+
canvas: !!x.canvasConfig,
|
|
41
|
+
};
|
|
42
|
+
}).sort(byName);
|
|
43
|
+
|
|
44
|
+
const authProviders = files(roots, 'authproviders', '.authprovider-meta.xml').map((f) => {
|
|
45
|
+
const x = readXml(f);
|
|
46
|
+
return { name: baseName(f, '.authprovider-meta.xml'), label: str(x.friendlyName), type: str(x.providerType), registrationHandler: str(x.registrationHandler) };
|
|
47
|
+
}).sort(byName);
|
|
48
|
+
|
|
49
|
+
const cspTrustedSites = files(roots, 'cspTrustedSites', '.cspTrustedSite-meta.xml').map((f) => {
|
|
50
|
+
const x = readXml(f);
|
|
51
|
+
return { name: baseName(f, '.cspTrustedSite-meta.xml'), host: host(str(x.endpointUrl)), context: str(x.context), active: x.isActive == null ? true : bool(x.isActive) };
|
|
52
|
+
}).sort(byName);
|
|
53
|
+
|
|
54
|
+
const corsOrigins = files(roots, 'corsWhitelistOrigins', '.corsWhitelistOrigin-meta.xml').map((f) => {
|
|
55
|
+
const x = readXml(f);
|
|
56
|
+
return { name: baseName(f, '.corsWhitelistOrigin-meta.xml'), host: host(str(x.urlPattern)) || str(x.urlPattern) };
|
|
57
|
+
}).sort(byName);
|
|
58
|
+
|
|
59
|
+
const callCenters = files(roots, 'callCenters', '.callCenter-meta.xml').map((f) => {
|
|
60
|
+
const x = readXml(f);
|
|
61
|
+
return { name: baseName(f, '.callCenter-meta.xml'), label: str(x.displayName), adapterHost: host(str(x.adapterUrl)), type: str(x.contactCenterChannels) ? 'contact-center' : 'open-cti' };
|
|
62
|
+
}).sort(byName);
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
namedCredentials,
|
|
66
|
+
externalCredentials,
|
|
67
|
+
remoteSites,
|
|
68
|
+
connectedApps,
|
|
69
|
+
authProviders,
|
|
70
|
+
cspTrustedSites,
|
|
71
|
+
corsOrigins,
|
|
72
|
+
callCenters,
|
|
73
|
+
externalServices: files(roots, 'externalServiceRegistrations', '.externalServiceRegistration-meta.xml').map((f) => baseName(f, '.externalServiceRegistration-meta.xml')).sort(),
|
|
74
|
+
externalDataSources: files(roots, 'dataSources', '.dataSource-meta.xml').map((f) => baseName(f, '.dataSource-meta.xml')).sort(),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { files, bundles, baseName, readXml, arr, str, hasFolder, safeRead } from '../xml.js';
|
|
4
|
+
|
|
5
|
+
export function extractMisc(roots) {
|
|
6
|
+
const labels = files(roots, 'labels', '.labels-meta.xml').flatMap((f) => arr(readXml(f).labels).map((l) => ({ name: str(l.fullName), category: str(l.categories) })));
|
|
7
|
+
const reports = files(roots, 'reports', '.report-meta.xml', { recursive: true });
|
|
8
|
+
const reportFolders = [...new Set(reports.map((f) => path.basename(path.dirname(f))))].sort();
|
|
9
|
+
const dashboards = files(roots, 'dashboards', '.dashboard-meta.xml', { recursive: true });
|
|
10
|
+
const emailTemplates = files(roots, 'email', '.email-meta.xml', { recursive: true });
|
|
11
|
+
const settings = files(roots, 'settings', '.settings-meta.xml').map((f) => baseName(f, '.settings-meta.xml')).sort();
|
|
12
|
+
const installedPackages = files(roots, 'installedPackages', '.installedPackage-meta.xml').map((f) => ({ namespace: baseName(f, '.installedPackage-meta.xml'), version: str(readXml(f).versionNumber) }))
|
|
13
|
+
.sort((a, b) => a.namespace.localeCompare(b.namespace));
|
|
14
|
+
|
|
15
|
+
const count = (folder, suffix) => files(roots, folder, suffix, { recursive: true }).length;
|
|
16
|
+
const signals = {
|
|
17
|
+
serviceChannels: count('serviceChannels', '.serviceChannel-meta.xml'),
|
|
18
|
+
routingConfigs: count('queueRoutingConfigs', '.queueRoutingConfig-meta.xml'),
|
|
19
|
+
presenceStatuses: count('servicePresenceStatuses', '.servicePresenceStatus-meta.xml'),
|
|
20
|
+
presenceConfigs: count('presenceUserConfigs', '.presenceUserConfig-meta.xml'),
|
|
21
|
+
networks: count('networks', '.network-meta.xml'),
|
|
22
|
+
sites: count('sites', '.site-meta.xml'),
|
|
23
|
+
experiences: bundles(roots, 'experiences').length + bundles(roots, 'digitalExperiences').length,
|
|
24
|
+
bots: bundles(roots, 'bots').length,
|
|
25
|
+
genAi: count('genAiPlanners', '') + count('genAiPlannerBundles', '') + count('genAiPlugins', '') + count('genAiFunctions', '') + bundles(roots, 'aiAuthoringBundles').length,
|
|
26
|
+
entitlementProcesses: count('entitlementProcesses', ''),
|
|
27
|
+
dataStreams: count('dataStreamDefinitions', ''),
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// Components documented by name only (routing, channels, sites, paths, rules, agents…)
|
|
31
|
+
const NAMED = ['serviceChannels', 'queueRoutingConfigs', 'servicePresenceStatuses', 'presenceUserConfigs', 'presenceDeclineReasons', 'networks', 'sites',
|
|
32
|
+
'entitlementProcesses', 'milestoneTypes', 'pathAssistants', 'duplicateRules', 'matchingRules', 'approvalProcesses', 'notificationtypes', 'genAiPlanners',
|
|
33
|
+
'genAiPlugins', 'genAiFunctions', 'genAiPromptTemplates', 'brandingSets', 'navigationMenus', 'bots', 'experiences', 'digitalExperiences', 'aiAuthoringBundles', 'workflows', 'globalValueSets'];
|
|
34
|
+
const named = {};
|
|
35
|
+
for (const folder of NAMED) {
|
|
36
|
+
const list = [...files(roots, folder, '').map((f) => path.basename(f).split('.')[0]), ...bundles(roots, folder).map((b) => b.name)];
|
|
37
|
+
if (list.length) named[folder] = [...new Set(list)].sort();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
named,
|
|
42
|
+
labels: labels.filter((l) => l.name).sort((a, b) => a.name.localeCompare(b.name)),
|
|
43
|
+
staticResources: files(roots, 'staticresources', '.resource-meta.xml').map((f) => baseName(f, '.resource-meta.xml')).sort(),
|
|
44
|
+
reports: { total: reports.length, folders: reportFolders, byFolder: Object.fromEntries(reportFolders.map((fo) => [fo, reports.filter((f) => path.basename(path.dirname(f)) === fo).length])), names: reports.map((f) => baseName(f, '.report-meta.xml')).sort() },
|
|
45
|
+
dashboards: { total: dashboards.length, names: dashboards.map((f) => baseName(f, '.dashboard-meta.xml')).sort() },
|
|
46
|
+
reportTypes: files(roots, 'reportTypes', '.reportType-meta.xml').map((f) => baseName(f, '.reportType-meta.xml')).sort(),
|
|
47
|
+
emailTemplates: emailTemplates.map((f) => baseName(f, '.email-meta.xml')).sort(),
|
|
48
|
+
settings,
|
|
49
|
+
installedPackages,
|
|
50
|
+
signals,
|
|
51
|
+
folders: metadataFolders(roots),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Every metadata folder present, with its file count — a raw inventory for appendices. */
|
|
56
|
+
function metadataFolders(roots) {
|
|
57
|
+
const counts = {};
|
|
58
|
+
for (const r of roots) {
|
|
59
|
+
for (const e of fs.readdirSync(r, { withFileTypes: true })) {
|
|
60
|
+
if (!e.isDirectory()) continue;
|
|
61
|
+
counts[e.name] = (counts[e.name] || 0) + files([r], e.name, '-meta.xml', { recursive: true }).length;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return counts;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const NS_PRODUCTS = {
|
|
68
|
+
FinServ: 'Financial Services Cloud',
|
|
69
|
+
HealthCloudGA: 'Health Cloud',
|
|
70
|
+
npsp: 'Nonprofit Success Pack', npe01: 'Nonprofit Success Pack', npo02: 'Nonprofit Success Pack', npe03: 'Nonprofit Success Pack', npe4: 'Nonprofit Success Pack', npe5: 'Nonprofit Success Pack',
|
|
71
|
+
SBQQ: 'Salesforce CPQ', blng: 'Salesforce Billing',
|
|
72
|
+
FSL: 'Field Service',
|
|
73
|
+
et4ae5: 'Marketing Cloud Connect',
|
|
74
|
+
pi: 'Marketing Cloud Account Engagement',
|
|
75
|
+
vlocity_cmt: 'OmniStudio (Communications, Media & Energy)', vlocity_ins: 'OmniStudio (Insurance)', vlocity_ps: 'OmniStudio (Public Sector)', omnistudio: 'OmniStudio',
|
|
76
|
+
dsfs: 'DocuSign eSignature', echosign_dev1: 'Adobe Acrobat Sign', mkto_si: 'Marketo Sales Insight',
|
|
77
|
+
sf_chttr_apps: 'Chatter apps', LightningMDE: 'Lightning Markdown Editor', pse: 'Certinia PSA', c2g: 'Certinia Financials',
|
|
78
|
+
sfLma: 'License Management App', CHANNEL_ORDERS: 'Channel Order App', wave: 'CRM Analytics',
|
|
79
|
+
cgcloud: 'Consumer Goods Cloud', hed: 'Education Data Architecture', slackv2: 'Slack for Salesforce', amazonconnect: 'Amazon Connect CTI Adapter',
|
|
80
|
+
sbaa: 'Salesforce Advanced Approvals', APXTConga4: 'Conga Composer', Apttus_Config2: 'Conga CPQ', rh2: 'Rollup Helper', dlrs: 'Declarative Lookup Rollup Summaries',
|
|
81
|
+
SDOC: 'S-Docs', geopointe: 'Geopointe', fferpcore: 'Certinia ERP Core', sf_com_apps: 'Salesforce Connected Apps', LID: 'LinkedIn Sales Navigator', ltnadptn: 'Lightning Adoption', maps: 'Salesforce Maps', LLC_BI: 'nCino', Loan: 'nCino (Loan)',
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
/** Namespaces referenced by metadata (managed packages), with a product name when known. */
|
|
85
|
+
export function detectNamespaces(roots, installedPackages) {
|
|
86
|
+
const refs = {};
|
|
87
|
+
const re = /\b([A-Za-z][A-Za-z0-9]{1,14})__[A-Za-z][A-Za-z0-9_]*?__(c|r|mdt|e|x|b)\b/g;
|
|
88
|
+
for (const f of files(roots, '', '', { recursive: true }).filter((f) => /\.(xml|cls|trigger|js|html|page|cmp|json)$/.test(f))) {
|
|
89
|
+
for (const m of safeRead(f).matchAll(re)) refs[m[1]] = (refs[m[1]] || 0) + 1;
|
|
90
|
+
}
|
|
91
|
+
for (const p of installedPackages) refs[p.namespace] = refs[p.namespace] || 0;
|
|
92
|
+
return Object.keys(refs).sort().map((ns) => ({
|
|
93
|
+
namespace: ns,
|
|
94
|
+
references: refs[ns],
|
|
95
|
+
product: NS_PRODUCTS[ns] || null,
|
|
96
|
+
installed: installedPackages.some((p) => p.namespace === ns),
|
|
97
|
+
}));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export const hasAny = (roots, folders) => folders.some((f) => hasFolder(roots, f));
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { files, baseName, readXml, arr, bool, str, byName } from '../xml.js';
|
|
2
|
+
|
|
3
|
+
// System permissions worth surfacing in a review (powerful or sensitive).
|
|
4
|
+
const POWERFUL = ['ModifyAllData', 'ViewAllData', 'ManageUsers', 'AuthorApex', 'CustomizeApplication', 'ManageProfilesPermissionsets', 'ViewSetup', 'ApiEnabled', 'ManageSharing', 'ExportReport', 'ModifyMetadata', 'ManageInternalUsers', 'ResetPasswords', 'PasswordNeverExpires'];
|
|
5
|
+
|
|
6
|
+
function enabledPowerful(x) {
|
|
7
|
+
return arr(x.userPermissions).filter((p) => bool(p.enabled) && POWERFUL.includes(str(p.name))).map((p) => str(p.name)).sort();
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function extractSecurity(roots) {
|
|
11
|
+
const profiles = files(roots, 'profiles', '.profile-meta.xml').map((f) => {
|
|
12
|
+
const x = readXml(f);
|
|
13
|
+
return {
|
|
14
|
+
name: baseName(f, '.profile-meta.xml'),
|
|
15
|
+
userLicense: str(x.userLicense),
|
|
16
|
+
custom: bool(x.custom),
|
|
17
|
+
objectPermissions: arr(x.objectPermissions).length,
|
|
18
|
+
fieldPermissions: arr(x.fieldPermissions).length,
|
|
19
|
+
powerful: enabledPowerful(x),
|
|
20
|
+
loginIpRanges: arr(x.loginIpRanges).length,
|
|
21
|
+
};
|
|
22
|
+
}).sort(byName);
|
|
23
|
+
|
|
24
|
+
const permissionSets = files(roots, 'permissionsets', '.permissionset-meta.xml').map((f) => {
|
|
25
|
+
const x = readXml(f);
|
|
26
|
+
return {
|
|
27
|
+
name: baseName(f, '.permissionset-meta.xml'),
|
|
28
|
+
label: str(x.label),
|
|
29
|
+
license: str(x.license),
|
|
30
|
+
objectPermissions: arr(x.objectPermissions).length,
|
|
31
|
+
fieldPermissions: arr(x.fieldPermissions).length,
|
|
32
|
+
classAccesses: arr(x.classAccesses).length,
|
|
33
|
+
customPermissions: arr(x.customPermissions).filter((c) => bool(c.enabled)).length,
|
|
34
|
+
powerful: enabledPowerful(x),
|
|
35
|
+
};
|
|
36
|
+
}).sort(byName);
|
|
37
|
+
|
|
38
|
+
const permissionSetGroups = files(roots, 'permissionsetgroups', '.permissionsetgroup-meta.xml').map((f) => {
|
|
39
|
+
const x = readXml(f);
|
|
40
|
+
return {
|
|
41
|
+
name: baseName(f, '.permissionsetgroup-meta.xml'),
|
|
42
|
+
label: str(x.label),
|
|
43
|
+
permissionSets: arr(x.permissionSets).map(String).sort(),
|
|
44
|
+
mutingPermissionSets: arr(x.mutingPermissionSets).map(String).sort(),
|
|
45
|
+
status: str(x.status),
|
|
46
|
+
};
|
|
47
|
+
}).sort(byName);
|
|
48
|
+
|
|
49
|
+
const roles = files(roots, 'roles', '.role-meta.xml').map((f) => {
|
|
50
|
+
const x = readXml(f);
|
|
51
|
+
return { name: baseName(f, '.role-meta.xml'), label: str(x.name), parent: str(x.parentRole) };
|
|
52
|
+
}).sort(byName);
|
|
53
|
+
|
|
54
|
+
const queues = files(roots, 'queues', '.queue-meta.xml').map((f) => {
|
|
55
|
+
const x = readXml(f);
|
|
56
|
+
return { name: baseName(f, '.queue-meta.xml'), label: str(x.name), objects: arr(x.queueSobject).map((q) => str(q.sobjectType)).filter(Boolean).sort() };
|
|
57
|
+
}).sort(byName);
|
|
58
|
+
|
|
59
|
+
const groups = files(roots, 'groups', '.group-meta.xml').map((f) => {
|
|
60
|
+
const x = readXml(f);
|
|
61
|
+
return { name: baseName(f, '.group-meta.xml'), label: str(x.name), includeBosses: bool(x.doesIncludeBosses) };
|
|
62
|
+
}).sort(byName);
|
|
63
|
+
|
|
64
|
+
const sharingRules = files(roots, 'sharingRules', '.sharingRules-meta.xml').map((f) => {
|
|
65
|
+
const x = readXml(f);
|
|
66
|
+
const rule = (r, kind) => ({ name: str(r.fullName), label: str(r.label), kind, access: str(r.accessLevel) });
|
|
67
|
+
const rules = [
|
|
68
|
+
...arr(x.sharingCriteriaRules).map((r) => rule(r, 'criteria')),
|
|
69
|
+
...arr(x.sharingOwnerRules).map((r) => rule(r, 'owner')),
|
|
70
|
+
...arr(x.sharingGuestRules).map((r) => rule(r, 'guest')),
|
|
71
|
+
...arr(x.sharingTerritoryRules).map((r) => rule(r, 'territory')),
|
|
72
|
+
].sort((a, b) => String(a.name).localeCompare(String(b.name)));
|
|
73
|
+
return { object: baseName(f, '.sharingRules-meta.xml'), rules };
|
|
74
|
+
}).filter((s) => s.rules.length).sort((a, b) => a.object.localeCompare(b.object));
|
|
75
|
+
|
|
76
|
+
const securitySettings = readXml(files(roots, 'settings', 'Security.settings-meta.xml')[0] || '');
|
|
77
|
+
const session = securitySettings.sessionSettings || {};
|
|
78
|
+
const pick = (o, keys) => Object.fromEntries(keys.filter((k) => o[k] != null).map((k) => [k, str(o[k])]));
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
profiles,
|
|
82
|
+
permissionSets,
|
|
83
|
+
permissionSetGroups,
|
|
84
|
+
roles,
|
|
85
|
+
queues,
|
|
86
|
+
groups,
|
|
87
|
+
sharingRules,
|
|
88
|
+
customPermissions: files(roots, 'customPermissions', '.customPermission-meta.xml').map((f) => baseName(f, '.customPermission-meta.xml')).sort(),
|
|
89
|
+
session: pick(session, ['sessionTimeout', 'forceLogoutOnSessionTimeout', 'lockSessionsToIp', 'lockSessionsToDomain', 'requireHttpOnly', 'enableClickjackNonsetupUser', 'enableCSRFOnGet', 'enableCSRFOnPost', 'enforceIpRangesEveryRequest', 'identityConfirmationOnTwoFactorRegistrationEnabled']),
|
|
90
|
+
passwordPolicies: pick(securitySettings.passwordPolicies || {}, ['complexity', 'expiration', 'historyRestriction', 'lockoutInterval', 'maxLoginAttempts', 'minimumPasswordLength']),
|
|
91
|
+
};
|
|
92
|
+
}
|