@salesforce/afv-skills 1.44.0 → 1.46.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.
Files changed (109) hide show
  1. package/package.json +1 -1
  2. package/skills/automation-flow-generate/SKILL.md +11 -5
  3. package/skills/consumer-goods-rtr-datacloud-export-configure/SKILL.md +72 -0
  4. package/skills/consumer-goods-rtr-datacloud-export-configure/references/inputs-and-namespace.md +38 -0
  5. package/skills/consumer-goods-rtr-datacloud-export-configure/references/procedure.md +158 -0
  6. package/skills/consumer-goods-rtr-datacloud-export-configure/scripts/detect-namespace.js +86 -0
  7. package/skills/consumer-goods-rtr-datacloud-export-configure/scripts/render-apex.js +64 -0
  8. package/skills/consumer-goods-rtr-datacloud-export-configure/scripts/resolve-id-by-name.js +47 -0
  9. package/skills/consumer-goods-rtr-datacloud-export-configure/scripts/sf-rest.js +171 -0
  10. package/skills/consumer-goods-rtr-datacloud-export-configure/scripts/soql-escape.js +26 -0
  11. package/skills/consumer-goods-rtr-datacloud-export-configure/scripts/upsert-report-config.apex +51 -0
  12. package/skills/consumer-goods-rtr-datacloud-export-configure/scripts/upsert-system-setting.apex +21 -0
  13. package/skills/consumer-goods-tpe-dashboard-configure/SKILL.md +74 -0
  14. package/skills/consumer-goods-tpe-dashboard-configure/references/phases-1-6.md +112 -0
  15. package/skills/consumer-goods-tpe-dashboard-configure/references/phases-7-12.md +157 -0
  16. package/skills/consumer-goods-tpe-dashboard-configure/scripts/find-failure-reason.js +132 -0
  17. package/skills/consumer-goods-tpe-dashboard-configure/scripts/poll-status.js +116 -0
  18. package/skills/consumer-goods-tpe-dashboard-configure/scripts/render-apex.js +64 -0
  19. package/skills/consumer-goods-tpe-dashboard-configure/scripts/run-data-transform.js +121 -0
  20. package/skills/consumer-goods-tpe-dashboard-configure/scripts/schedule-business-period-export.apex +27 -0
  21. package/skills/consumer-goods-tpe-dashboard-configure/scripts/sf-rest.js +171 -0
  22. package/skills/consumer-goods-tpe-dashboard-configure/scripts/soql-escape.js +25 -0
  23. package/skills/consumer-goods-tpe-dashboard-custom-kpi-configure/SKILL.md +141 -0
  24. package/skills/consumer-goods-tpe-dashboard-custom-kpi-configure/references/payload-shapes.md +447 -0
  25. package/skills/consumer-goods-tpe-dashboard-custom-kpi-configure/references/procedure.md +263 -0
  26. package/skills/consumer-goods-tpe-dashboard-custom-kpi-configure/scripts/clone-tpe-dashboards.js +537 -0
  27. package/skills/consumer-goods-tpe-dashboard-custom-kpi-configure/scripts/sf-rest.js +195 -0
  28. package/skills/consumer-goods-tpe-datakit-deploy/SKILL.md +157 -0
  29. package/skills/consumer-goods-tpe-datakit-deploy/scripts/detect-namespace.js +86 -0
  30. package/skills/consumer-goods-tpe-datakit-deploy/scripts/download-static-resource.js +151 -0
  31. package/skills/consumer-goods-tpe-datakit-deploy/scripts/extract-crm-field-permissions.js +115 -0
  32. package/skills/consumer-goods-tpe-datakit-deploy/scripts/sf-rest.js +109 -0
  33. package/skills/consumer-goods-tpe-datakit-deploy/scripts/update-field-permissions.js +433 -0
  34. package/skills/dx-code-analyzer-configure/scripts/validate-config.sh +14 -10
  35. package/skills/dx-code-analyzer-run/scripts/apply-fixes.js +45 -4
  36. package/skills/dx-code-analyzer-run/scripts/describe-rule.js +52 -32
  37. package/skills/platform-apex-logs-debug/SKILL.md +7 -7
  38. package/skills/platform-custom-application-generate/SKILL.md +4 -4
  39. package/skills/platform-custom-object-generate/SKILL.md +7 -7
  40. package/skills/platform-custom-tab-generate/SKILL.md +1 -1
  41. package/skills/platform-flexipage-generate/SKILL.md +4 -0
  42. package/skills/platform-list-view-generate/SKILL.md +1 -0
  43. package/skills/platform-soql-query/SKILL.md +8 -8
  44. package/skills/platform-value-set-generate/SKILL.md +2 -2
  45. package/skills/service-catalog-template-coordinate/SKILL.md +263 -0
  46. package/skills/service-catalog-template-coordinate/examples/output-templates.md +44 -0
  47. package/skills/service-catalog-template-coordinate/references/mcp-invocation.md +183 -0
  48. package/skills/service-catalog-template-coordinate/references/operations.md +230 -0
  49. package/skills/service-itsm-agentic-setup-agent-runtime-access-assign/SKILL.md +243 -0
  50. package/skills/service-itsm-agentic-setup-agent-runtime-access-assign/references/cli-invocation.md +205 -0
  51. package/skills/service-itsm-agentic-setup-agent-runtime-access-assign/references/helper-contracts.md +236 -0
  52. package/skills/service-itsm-agentic-setup-agent-runtime-access-assign/references/permset-topology.md +132 -0
  53. package/skills/service-itsm-agentic-setup-agent-runtime-access-assign/scripts/classify-activated-agents.mjs +106 -0
  54. package/skills/service-itsm-agentic-setup-agent-runtime-access-assign/scripts/classify-agent-access-state.mjs +113 -0
  55. package/skills/service-itsm-agentic-setup-agent-runtime-access-assign/scripts/classify-assignment-state.mjs +99 -0
  56. package/skills/service-itsm-agentic-setup-agent-runtime-access-assign/scripts/classify-platform-permset-availability.mjs +155 -0
  57. package/skills/service-itsm-agentic-setup-agent-runtime-access-assign/scripts/gate-unified-catalog-tiers.mjs +100 -0
  58. package/skills/service-itsm-agentic-setup-agent-runtime-access-assign/scripts/rank-candidate-users.mjs +95 -0
  59. package/skills/service-itsm-agentic-setup-agent-runtime-access-assign/scripts/resolve-target-user.mjs +86 -0
  60. package/skills/service-itsm-agentic-setup-agentforce-coordinate/SKILL.md +44 -23
  61. package/skills/service-itsm-agentic-setup-agentforce-coordinate/examples/output-templates.md +33 -9
  62. package/skills/service-itsm-agentic-setup-agentforce-studio-configure/SKILL.md +17 -18
  63. package/skills/service-itsm-agentic-setup-cmdb-coordinate/SKILL.md +3 -1
  64. package/skills/service-itsm-agentic-setup-configure/SKILL.md +20 -12
  65. package/skills/service-itsm-agentic-setup-configure/examples/output-templates.md +73 -5
  66. package/skills/service-itsm-agentic-setup-employee-agent-configure/SKILL.md +8 -7
  67. package/skills/service-itsm-agentic-setup-employee-agent-configure/references/cli-invocation.md +45 -33
  68. package/skills/service-itsm-agentic-setup-employee-agent-configure/references/reactivation.md +8 -6
  69. package/skills/service-itsm-agentic-setup-employee-agent-configure/references/workflow-detail.md +10 -10
  70. package/skills/service-itsm-agentic-setup-employee-agent-configure/scripts/classify-agent-existence.mjs +114 -56
  71. package/skills/service-itsm-agentic-setup-employee-agent-configure/scripts/classify-preflight.mjs +33 -17
  72. package/skills/service-itsm-agentic-setup-employee-agent-configure/scripts/render-report.mjs +9 -3
  73. package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/SKILL.md +8 -7
  74. package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/references/cli-invocation.md +43 -32
  75. package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/references/reactivation.md +6 -4
  76. package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/references/workflow-detail.md +10 -10
  77. package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/scripts/classify-agent-existence.mjs +106 -55
  78. package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/scripts/classify-preflight.mjs +27 -13
  79. package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/scripts/render-report.mjs +9 -3
  80. package/skills/service-itsm-agentic-setup-incident-sla-configure/SKILL.md +159 -161
  81. package/skills/service-itsm-agentic-setup-incident-sla-configure/assets/attach-milestone-action.json +51 -0
  82. package/skills/service-itsm-agentic-setup-incident-sla-configure/assets/attach-milestone.json +1 -1
  83. package/skills/service-itsm-agentic-setup-incident-sla-configure/assets/predefined-incident-policy.json +120 -0
  84. package/skills/service-itsm-agentic-setup-incident-sla-configure/examples/milestone-patterns.md +28 -5
  85. package/skills/service-itsm-agentic-setup-incident-sla-configure/examples/output-templates.md +19 -1
  86. package/skills/service-itsm-agentic-setup-incident-sla-configure/references/mcp-invocation.md +350 -30
  87. package/skills/service-itsm-channels-coordinate/SKILL.md +80 -213
  88. package/skills/service-itsm-slack-itservice-configure/SKILL.md +363 -0
  89. package/skills/service-itsm-slack-itservice-configure/references/connect-agentforce-to-slack.md +159 -0
  90. package/skills/service-itsm-slack-itservice-configure/references/manage-slack-connection.md +88 -0
  91. package/skills/service-itsm-slack-itservice-configure/references/manage-user-access.md +117 -0
  92. package/skills/service-itsm-slack-itservice-configure/references/record-visibility.md +78 -0
  93. package/skills/service-itsm-slack-itservice-configure/references/site-membership-verification.md +126 -0
  94. package/skills/service-itsm-slack-itservice-configure/scripts/classify-user-access.mjs +167 -0
  95. package/skills/service-itsm-teams-configure/SKILL.md +50 -47
  96. package/skills/service-itsm-teams-configure/references/azure-credential-population.md +42 -28
  97. package/skills/service-itsm-teams-configure/references/gotchas.md +1 -2
  98. package/skills/service-itsm-teams-coordinate/SKILL.md +22 -18
  99. package/skills/service-itsm-teams-coordinate/examples/output-templates.md +12 -9
  100. package/skills/service-itsm-teams-itdesk-configure/SKILL.md +60 -44
  101. package/skills/service-itsm-teams-itservice-configure/SKILL.md +56 -70
  102. package/skills/service-catalog-template-deploy/SKILL.md +0 -310
  103. package/skills/service-catalog-template-deploy/references/cli-invocation.md +0 -258
  104. package/skills/service-catalog-template-deploy/scripts/activate-verify.mjs +0 -164
  105. package/skills/service-catalog-template-deploy/scripts/build-deploy-payload.mjs +0 -94
  106. package/skills/service-catalog-template-deploy/scripts/resolve-template.mjs +0 -331
  107. package/skills/service-catalog-template-search/SKILL.md +0 -212
  108. package/skills/service-catalog-template-search/references/cli-invocation.md +0 -128
  109. package/skills/service-catalog-template-search/scripts/classify-catalog.mjs +0 -205
@@ -0,0 +1,115 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Extracts the exact {sobject, fields[]} list the TPM Accruals Data Kit
4
+ * reads from Salesforce, straight out of the downloaded package's
5
+ * `dataSourceObjects` metadata — so the field-permissions grant step never
6
+ * has to guess or hardcode which objects/fields matter. Scans every
7
+ * `*.dataSourceObject-meta.xml` in the given directory, keeps only the ones
8
+ * with `<dataSource>Salesforce_Home</dataSource>` (the CRM streams; every
9
+ * other `dataSource` value in that folder is a Data Cloud-side or
10
+ * already-computed object, not a CRM read), and for each one collects
11
+ * `<externalName>` from every `<dataSourceFields>` block EXCEPT the
12
+ * `isFormula=true` ones (`DataSource`, `DataSourceObject` — computed
13
+ * literals baked into the package, never real Salesforce fields).
14
+ * `<externalName>` values are already namespace-resolved for the org this
15
+ * package was downloaded from — never re-prefix them.
16
+ *
17
+ * Deliberately regex-based, not a full XML parser — this metadata format is
18
+ * simple/flat enough (no nested same-name tags, no CDATA) and this keeps the
19
+ * skill dependency-free per this repo's self-contained-skills rule.
20
+ *
21
+ * Usage:
22
+ * node extract-crm-field-permissions.js --dir <path-to-dataSourceObjects>
23
+ *
24
+ * Prints one line of JSON to stdout: [{"sobject":"Account","fields":[...]}, ...]
25
+ * Exit codes: 0 = success (including a directory with zero Salesforce_Home
26
+ * files — an empty array is printed), 1 = usage/read error.
27
+ */
28
+
29
+ const fs = require('fs');
30
+ const path = require('path');
31
+
32
+ function parseArgs(argv) {
33
+ const args = {};
34
+ for (let i = 0; i < argv.length; i += 1) {
35
+ const token = argv[i];
36
+ if (!token.startsWith('--')) continue;
37
+ args[token.slice(2)] = argv[i + 1];
38
+ i += 1;
39
+ }
40
+ return args;
41
+ }
42
+
43
+ function requireArg(args, name) {
44
+ if (!args[name]) {
45
+ console.error(`Missing required --${name}`);
46
+ process.exit(1);
47
+ }
48
+ return args[name];
49
+ }
50
+
51
+ function unescapeXml(value) {
52
+ return value
53
+ .replace(/&apos;/g, "'")
54
+ .replace(/&quot;/g, '"')
55
+ .replace(/&lt;/g, '<')
56
+ .replace(/&gt;/g, '>')
57
+ .replace(/&amp;/g, '&');
58
+ }
59
+
60
+ function extractTag(xml, tagName) {
61
+ const match = new RegExp(`<${tagName}>([^<]*)</${tagName}>`).exec(xml);
62
+ return match ? unescapeXml(match[1]) : undefined;
63
+ }
64
+
65
+ function parseDataSourceObjectXml(xml) {
66
+ const dataSource = extractTag(xml, 'dataSource');
67
+ const sobject = extractTag(xml, 'externalRecordIdentifier');
68
+ const fields = [];
69
+ const fieldBlockRe = /<dataSourceFields>([\s\S]*?)<\/dataSourceFields>/g;
70
+ let blockMatch;
71
+ while ((blockMatch = fieldBlockRe.exec(xml))) {
72
+ const block = blockMatch[1];
73
+ const isFormula = extractTag(block, 'isFormula') === 'true';
74
+ const externalName = extractTag(block, 'externalName');
75
+ if (!isFormula && externalName) {
76
+ fields.push(externalName);
77
+ }
78
+ }
79
+ return { dataSource, sobject, fields };
80
+ }
81
+
82
+ function main() {
83
+ const args = parseArgs(process.argv.slice(2));
84
+ const dir = requireArg(args, 'dir');
85
+
86
+ let files;
87
+ try {
88
+ files = fs.readdirSync(dir).filter((f) => f.endsWith('.dataSourceObject-meta.xml'));
89
+ } catch (error) {
90
+ console.error(`extract-crm-field-permissions: could not read --dir "${dir}" — ${error.message}`);
91
+ process.exit(1);
92
+ }
93
+
94
+ const bySobject = new Map();
95
+ for (const file of files) {
96
+ const xml = fs.readFileSync(path.join(dir, file), 'utf8');
97
+ const { dataSource, sobject, fields } = parseDataSourceObjectXml(xml);
98
+ if (dataSource !== 'Salesforce_Home' || !sobject || !fields.length) continue;
99
+ const existing = bySobject.get(sobject) || new Set();
100
+ fields.forEach((f) => existing.add(f));
101
+ bySobject.set(sobject, existing);
102
+ }
103
+
104
+ const result = Array.from(bySobject.entries()).map(([sobject, fields]) => ({
105
+ sobject,
106
+ fields: Array.from(fields).sort(),
107
+ }));
108
+ process.stdout.write(JSON.stringify(result));
109
+ }
110
+
111
+ if (require.main === module) {
112
+ main();
113
+ }
114
+
115
+ module.exports = { parseDataSourceObjectXml };
@@ -0,0 +1,109 @@
1
+ /**
2
+ * Shared `sf` CLI helpers for this skill's scripts: running `sf ... --json`
3
+ * commands and resolving an org's access token.
4
+ *
5
+ * `sf org display --json`'s `result.accessToken` stopped being a real token
6
+ * as of CLI v2.136.8 — that field becomes a redacted hint string pointing at
7
+ * `sf org auth show-access-token` instead. `getAccessToken()` is version-gated
8
+ * to handle both cases. The token is never printed.
9
+ *
10
+ * Also runnable directly for two subcommands that print only safe,
11
+ * non-secret fields from `sf org display --json` — that command's `result`
12
+ * includes `accessToken`, so nothing in this skill should run it directly
13
+ * and surface the raw output; these strip it before printing:
14
+ * node sf-rest.js org-status --target-org <username>
15
+ * -> {"username","alias","connectedStatus","orgId"} ; exit 1 if not Connected
16
+ */
17
+
18
+ const { execFileSync } = require('child_process');
19
+
20
+ const ACCESS_TOKEN_VIA_SHOW_COMMAND_SINCE = [2, 136, 8];
21
+
22
+ function sfJson(cliArgs) {
23
+ const raw = execFileSync('sf', cliArgs, { encoding: 'utf8' });
24
+ return JSON.parse(raw).result;
25
+ }
26
+
27
+ function getCliVersion() {
28
+ // Unlike every other `sf ... --json` command, `sf version --json` returns
29
+ // its fields at the top level — no `{status, result}` envelope — so this
30
+ // can't go through `sfJson`.
31
+ const raw = execFileSync('sf', ['version', '--json'], { encoding: 'utf8' });
32
+ const { cliVersion } = JSON.parse(raw);
33
+ const match = cliVersion.match(/(\d+)\.(\d+)\.(\d+)/);
34
+ // A pre-release/suffixed version string (e.g. a nightly build) that
35
+ // doesn't match this pattern is treated as "new enough" below, via
36
+ // isAtLeast's null passthrough — never silently fall back to the
37
+ // redacted-token path on an unparseable version.
38
+ return match ? match.slice(1).map(Number) : null;
39
+ }
40
+
41
+ function isAtLeast(version, minVersion) {
42
+ if (!version) return true;
43
+ for (let i = 0; i < minVersion.length; i += 1) {
44
+ const a = version[i] || 0;
45
+ const b = minVersion[i] || 0;
46
+ if (a !== b) return a > b;
47
+ }
48
+ return true;
49
+ }
50
+
51
+ function getAccessToken(targetOrg) {
52
+ if (isAtLeast(getCliVersion(), ACCESS_TOKEN_VIA_SHOW_COMMAND_SINCE)) {
53
+ return sfJson([
54
+ 'org', 'auth', 'show-access-token',
55
+ '--target-org', targetOrg, '--no-prompt', '--json',
56
+ ]).accessToken;
57
+ }
58
+ return sfJson(['org', 'display', '--target-org', targetOrg, '--json']).accessToken;
59
+ }
60
+
61
+ function getOrgStatus(targetOrg) {
62
+ const info = sfJson(['org', 'display', '--target-org', targetOrg, '--json']);
63
+ return {
64
+ username: info.username,
65
+ alias: info.alias || null,
66
+ connectedStatus: info.connectedStatus,
67
+ orgId: info.id,
68
+ };
69
+ }
70
+
71
+ function parseArgs(argv) {
72
+ const args = {};
73
+ for (let i = 0; i < argv.length; i += 1) {
74
+ const token = argv[i];
75
+ if (!token.startsWith('--')) continue;
76
+ args[token.slice(2)] = argv[i + 1];
77
+ i += 1;
78
+ }
79
+ return args;
80
+ }
81
+
82
+ function requireArg(args, name) {
83
+ if (!args[name]) {
84
+ console.error(`Missing required --${name}`);
85
+ process.exit(2);
86
+ }
87
+ return args[name];
88
+ }
89
+
90
+ function main() {
91
+ const [command, ...rest] = process.argv.slice(2);
92
+ const args = parseArgs(rest);
93
+ const targetOrg = requireArg(args, 'target-org');
94
+
95
+ if (command === 'org-status') {
96
+ const status = getOrgStatus(targetOrg);
97
+ process.stdout.write(JSON.stringify(status));
98
+ if (status.connectedStatus !== 'Connected') process.exit(1);
99
+ return;
100
+ }
101
+ console.error(`Unknown command: ${command}. Use "org-status"`);
102
+ process.exit(2);
103
+ }
104
+
105
+ if (require.main === module) {
106
+ main();
107
+ }
108
+
109
+ module.exports = { sfJson, getCliVersion, isAtLeast, getAccessToken, getOrgStatus };
@@ -0,0 +1,433 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Grants FieldPermissions rows, and object-level ObjectPermissions
4
+ * (PermissionsRead + PermissionsViewAllRecords), on a permission set for the
5
+ * {sobject, fields[]} list produced by extract-crm-field-permissions.js.
6
+ * Add-only by design at both levels: an existing row/flag already granting
7
+ * access is left untouched, and this script never deletes a row or flips a
8
+ * granted boolean back to false — the only operations are "insert a new
9
+ * row" (nothing granted yet) and "flip an existing false flag to true"
10
+ * (row exists but the flag is explicitly off). Neither operation ever
11
+ * removes access, and object-level writes never touch PermissionsCreate/
12
+ * Edit/Delete/ModifyAllRecords — only Read and ViewAllRecords are ever set,
13
+ * and only ever to true. The caller (this skill's Phase 2) is responsible
14
+ * for getting the user's explicit go-ahead before invoking this without
15
+ * --dry-run — this script performs whatever it's told, same as every other
16
+ * write helper in this skill.
17
+ *
18
+ * Reads (permission set lookup, existing FieldPermissions/ObjectPermissions)
19
+ * go through `sf data query`, consistent with the rest of this skill.
20
+ * Writes go through the sObject Collections API (POST/PATCH
21
+ * .../composite/sobjects, batches of 200) instead of one `sf data create
22
+ * record` process per field — dozens of fields across a dozen-plus objects
23
+ * would otherwise mean dozens of CLI spawns. The org's own `sf`-authenticated
24
+ * access token drives every call, never a hardcoded credential, and it is
25
+ * never printed.
26
+ *
27
+ * Usage:
28
+ * node update-field-permissions.js \
29
+ * --target-org <username> \
30
+ * --input <path-to-extract-output.json | -> \
31
+ * [--permission-set-name sfdc_a360_sfcrm_data_extract] \
32
+ * [--dry-run]
33
+ *
34
+ * --input accepts a file path or "-" to read the extract script's JSON
35
+ * array (`[{sobject, fields[]}, ...]`) from stdin.
36
+ *
37
+ * Before diffing fields, every field is checked against a live
38
+ * `sobject/describe` call for its object: fields absent from the object
39
+ * entirely, or present but not FLS-controllable (master-detail/required
40
+ * lookups, standard system fields like Id/Name/SystemModstamp — describe's
41
+ * own `permissionable: false`), are filtered out into `notFlsEligible`
42
+ * rather than attempted — no hardcoded field-name guessing. Object-level
43
+ * permissions have no such eligibility gate — every sobject in the input is
44
+ * a real object the data kit reads, so Read + ViewAllRecords are always
45
+ * in scope for it.
46
+ *
47
+ * Prints one line of JSON to stdout:
48
+ * {
49
+ * permissionSetId, permissionSetLabel, dryRun,
50
+ * alreadyGranted: [{sobject, field}],
51
+ * notFlsEligible: [{sobject, field, onObject}],
52
+ * granted / wouldGrant: [{sobject, field}],
53
+ * failed: [{sobject, field, error}],
54
+ * objectAlreadyGranted: [{sobject, permissionsRead, permissionsViewAllRecords}],
55
+ * objectGranted / objectWouldGrant: [{sobject, permissionsRead, permissionsViewAllRecords}],
56
+ * objectFailed: [{sobject, error}]
57
+ * }
58
+ * `failed`/`objectFailed` are exactly the manual-Setup punch list to hand
59
+ * back to the user when a grant can't be automated. `notFlsEligible` entries
60
+ * with `onObject: false` mean the package expects a field this org's object
61
+ * doesn't actually have — a package/org data mismatch, not something to
62
+ * grant manually. A small set of sobjects (currently just Product2 — see
63
+ * OBJECT_VIEWALL_INELIGIBLE) never get PermissionsViewAllRecords requested
64
+ * at all, because the org's own API rejects that specific flag with a
65
+ * license-constraint error; PermissionsRead is still granted normally for
66
+ * them. This is a known, permanent platform restriction, not a per-run
67
+ * failure — it's applied silently and surfaces as an ordinary
68
+ * `objectGranted`/`objectAlreadyGranted` entry with `permissionsViewAllRecords: false`,
69
+ * never as `objectFailed` and never something to ask the user about.
70
+ *
71
+ * Exit codes: 0 = no failures (includes dry-run and nothing-to-grant),
72
+ * 1 = permission set not found or bad input, 3 = one or more fields or
73
+ * objects failed to grant.
74
+ */
75
+
76
+ const fs = require('fs');
77
+ const { sfJson, getAccessToken } = require('./sf-rest');
78
+
79
+ function parseArgs(argv) {
80
+ const args = {};
81
+ for (let i = 0; i < argv.length; i += 1) {
82
+ const token = argv[i];
83
+ if (token === '--dry-run') {
84
+ args['dry-run'] = true;
85
+ continue;
86
+ }
87
+ if (!token.startsWith('--')) continue;
88
+ args[token.slice(2)] = argv[i + 1];
89
+ i += 1;
90
+ }
91
+ return args;
92
+ }
93
+
94
+ function requireArg(args, name) {
95
+ if (!args[name]) {
96
+ console.error(`Missing required --${name}`);
97
+ process.exit(1);
98
+ }
99
+ return args[name];
100
+ }
101
+
102
+ function readInput(inputArg) {
103
+ const raw = inputArg === '-' ? fs.readFileSync(0, 'utf8') : fs.readFileSync(inputArg, 'utf8');
104
+ let parsed;
105
+ try {
106
+ parsed = JSON.parse(raw);
107
+ } catch (error) {
108
+ console.error(`update-field-permissions: --input is not valid JSON — ${error.message}`);
109
+ process.exit(1);
110
+ }
111
+ if (!Array.isArray(parsed)) {
112
+ console.error('update-field-permissions: --input must be a JSON array of {sobject, fields[]}');
113
+ process.exit(1);
114
+ }
115
+ return parsed;
116
+ }
117
+
118
+ function soqlQuote(value) {
119
+ // Escape backslashes before quotes — otherwise a value ending in `\`
120
+ // combines with the following escaped quote to unescape itself.
121
+ return value.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
122
+ }
123
+
124
+ // Observed live: this org's license rejects any ObjectPermissions write that
125
+ // sets PermissionsViewAllRecords=true on Product2 —
126
+ // "c2c.integrationconstraints.LicenselessIntegrationPermConstrainer does not
127
+ // allow type=permission, label= View All Product2". Not a guess; a
128
+ // licenseless-integration-user platform restriction confirmed by the API's
129
+ // own error. PermissionsRead is unaffected and still granted normally —
130
+ // only ViewAllRecords is never requested for these sobjects, silently.
131
+ const OBJECT_VIEWALL_INELIGIBLE = new Set(['Product2']);
132
+
133
+ function chunk(list, size) {
134
+ const out = [];
135
+ for (let i = 0; i < list.length; i += size) out.push(list.slice(i, i + size));
136
+ return out;
137
+ }
138
+
139
+ async function describeSobject(instanceUrl, apiVersion, accessToken, sobject) {
140
+ const response = await fetch(`${instanceUrl}/services/data/v${apiVersion}/sobjects/${sobject}/describe`, {
141
+ headers: { Authorization: `Bearer ${accessToken}` },
142
+ });
143
+ if (!response.ok) {
144
+ const text = await response.text();
145
+ throw new Error(`describe ${sobject} failed — HTTP ${response.status}: ${text}`);
146
+ }
147
+ const { fields } = await response.json();
148
+ return new Map(fields.map((f) => [f.name, f.permissionable]));
149
+ }
150
+
151
+ async function postCollection(instanceUrl, apiVersion, accessToken, method, records) {
152
+ const response = await fetch(`${instanceUrl}/services/data/v${apiVersion}/composite/sobjects`, {
153
+ method,
154
+ headers: {
155
+ Authorization: `Bearer ${accessToken}`,
156
+ 'Content-Type': 'application/json',
157
+ },
158
+ body: JSON.stringify({ allOrNone: false, records }),
159
+ });
160
+ if (!response.ok) {
161
+ const text = await response.text();
162
+ throw new Error(`HTTP ${response.status}: ${text}`);
163
+ }
164
+ return response.json();
165
+ }
166
+
167
+ async function main() {
168
+ const args = parseArgs(process.argv.slice(2));
169
+ const targetOrg = requireArg(args, 'target-org');
170
+ const inputArg = requireArg(args, 'input');
171
+ const permissionSetName = args['permission-set-name'] || 'sfdc_a360_sfcrm_data_extract';
172
+ const dryRun = Boolean(args['dry-run']);
173
+
174
+ const entries = readInput(inputArg).filter(
175
+ (e) => e && e.sobject && Array.isArray(e.fields) && e.fields.length
176
+ );
177
+ if (!entries.length) {
178
+ process.stdout.write(JSON.stringify({ error: 'no_fields_in_input' }));
179
+ process.exit(1);
180
+ }
181
+
182
+ const psResult = sfJson([
183
+ 'data', 'query', '--target-org', targetOrg, '--json',
184
+ '--query', `SELECT Id, Label FROM PermissionSet WHERE Name = '${soqlQuote(permissionSetName)}'`,
185
+ ]);
186
+ if (!psResult.records || psResult.records.length < 1) {
187
+ process.stdout.write(JSON.stringify({ error: 'permission_set_not_found', permissionSetName }));
188
+ process.exit(1);
189
+ }
190
+ const permissionSetId = psResult.records[0].Id;
191
+ const permissionSetLabel = psResult.records[0].Label;
192
+
193
+ const sobjectList = entries.map((e) => `'${soqlQuote(e.sobject)}'`).join(',');
194
+ const existingResult = sfJson([
195
+ 'data', 'query', '--target-org', targetOrg, '--json',
196
+ '--query',
197
+ `SELECT Id, Field, SobjectType, PermissionsRead FROM FieldPermissions WHERE ParentId = '${permissionSetId}' AND SobjectType IN (${sobjectList})`,
198
+ ]);
199
+ const existingByField = new Map(
200
+ (existingResult.records || []).map((r) => [r.Field, r])
201
+ );
202
+
203
+ const existingObjectResult = sfJson([
204
+ 'data', 'query', '--target-org', targetOrg, '--json',
205
+ '--query',
206
+ `SELECT Id, SobjectType, PermissionsRead, PermissionsViewAllRecords FROM ObjectPermissions WHERE ParentId = '${permissionSetId}' AND SobjectType IN (${sobjectList})`,
207
+ ]);
208
+ const existingObjectBySobject = new Map(
209
+ (existingObjectResult.records || []).map((r) => [r.SobjectType, r])
210
+ );
211
+
212
+ const orgDetails = sfJson(['org', 'display', '--target-org', targetOrg, '--json']);
213
+ const instanceUrl = orgDetails.instanceUrl;
214
+ const apiVersion = orgDetails.apiVersion || '60.0';
215
+ const accessToken = getAccessToken(targetOrg);
216
+
217
+ const sobjectNames = [...new Set(entries.map((e) => e.sobject))];
218
+ const describeResults = await Promise.all(
219
+ sobjectNames.map((sobject) => describeSobject(instanceUrl, apiVersion, accessToken, sobject))
220
+ );
221
+ const describeBySobject = new Map(sobjectNames.map((sobject, i) => [sobject, describeResults[i]]));
222
+
223
+ const alreadyGranted = [];
224
+ const toInsert = [];
225
+ const toUpdate = [];
226
+ const notFlsEligible = [];
227
+ for (const entry of entries) {
228
+ const describeFields = describeBySobject.get(entry.sobject);
229
+ for (const field of entry.fields) {
230
+ if (!describeFields.has(field) || describeFields.get(field) !== true) {
231
+ // Not on the object at all, or present but not FLS-controllable
232
+ // (master-detail/required lookup, standard system field, etc.) —
233
+ // confirmed live from this org's own sobject describe.
234
+ notFlsEligible.push({ sobject: entry.sobject, field, onObject: describeFields.has(field) });
235
+ continue;
236
+ }
237
+ const fieldApiName = `${entry.sobject}.${field}`;
238
+ const existing = existingByField.get(fieldApiName);
239
+ if (existing && existing.PermissionsRead) {
240
+ alreadyGranted.push({ sobject: entry.sobject, field });
241
+ } else if (existing) {
242
+ // Row exists but Read is explicitly off — flip it on, never off.
243
+ toUpdate.push({ sobject: entry.sobject, field, fieldApiName, id: existing.Id });
244
+ } else {
245
+ toInsert.push({ sobject: entry.sobject, field, fieldApiName });
246
+ }
247
+ }
248
+ }
249
+ const missing = [...toInsert, ...toUpdate];
250
+
251
+ const objectAlreadyGranted = [];
252
+ const toInsertObject = [];
253
+ const toUpdateObject = [];
254
+ for (const sobject of new Set(entries.map((e) => e.sobject))) {
255
+ const viewAllEligible = !OBJECT_VIEWALL_INELIGIBLE.has(sobject);
256
+ const existing = existingObjectBySobject.get(sobject);
257
+ if (!existing) {
258
+ toInsertObject.push({ sobject, permissionsRead: true, permissionsViewAllRecords: viewAllEligible });
259
+ continue;
260
+ }
261
+ const needsRead = !existing.PermissionsRead;
262
+ const needsViewAll = viewAllEligible && !existing.PermissionsViewAllRecords;
263
+ if (!needsRead && !needsViewAll) {
264
+ objectAlreadyGranted.push({
265
+ sobject,
266
+ permissionsRead: existing.PermissionsRead,
267
+ permissionsViewAllRecords: viewAllEligible ? existing.PermissionsViewAllRecords : false,
268
+ });
269
+ } else {
270
+ // Row exists but Read and/or ViewAllRecords is explicitly off —
271
+ // flip only the off flag(s) on, never off, and never touch
272
+ // Create/Edit/Delete/ModifyAllRecords.
273
+ toUpdateObject.push({
274
+ sobject,
275
+ id: existing.Id,
276
+ permissionsRead: needsRead ? true : undefined,
277
+ permissionsViewAllRecords: needsViewAll ? true : undefined,
278
+ });
279
+ }
280
+ }
281
+ const missingObject = [...toInsertObject, ...toUpdateObject];
282
+
283
+ if (dryRun || (missing.length === 0 && missingObject.length === 0)) {
284
+ process.stdout.write(JSON.stringify({
285
+ permissionSetId,
286
+ permissionSetLabel,
287
+ dryRun,
288
+ alreadyGranted,
289
+ notFlsEligible,
290
+ wouldGrant: missing.map(({ sobject, field }) => ({ sobject, field })),
291
+ granted: [],
292
+ failed: [],
293
+ objectAlreadyGranted,
294
+ objectWouldGrant: missingObject.map(({ sobject, permissionsRead, permissionsViewAllRecords }) => ({
295
+ sobject,
296
+ permissionsRead: Boolean(permissionsRead),
297
+ permissionsViewAllRecords: Boolean(permissionsViewAllRecords),
298
+ })),
299
+ objectGranted: [],
300
+ objectFailed: [],
301
+ }));
302
+ process.exit(0);
303
+ }
304
+
305
+ const granted = [];
306
+ const failed = [];
307
+
308
+ for (const batch of chunk(toInsert, 200)) {
309
+ const records = batch.map((item) => ({
310
+ attributes: { type: 'FieldPermissions' },
311
+ ParentId: permissionSetId,
312
+ SobjectType: item.sobject,
313
+ Field: item.fieldApiName,
314
+ PermissionsRead: true,
315
+ }));
316
+ try {
317
+ const results = await postCollection(instanceUrl, apiVersion, accessToken, 'POST', records);
318
+ results.forEach((result, idx) => {
319
+ const item = batch[idx];
320
+ if (result.success) {
321
+ granted.push({ sobject: item.sobject, field: item.field });
322
+ } else {
323
+ const error = (result.errors || []).map((e) => e.message).join('; ') || 'unknown error';
324
+ failed.push({ sobject: item.sobject, field: item.field, error });
325
+ }
326
+ });
327
+ } catch (error) {
328
+ batch.forEach((item) => failed.push({ sobject: item.sobject, field: item.field, error: error.message }));
329
+ }
330
+ }
331
+
332
+ for (const batch of chunk(toUpdate, 200)) {
333
+ const records = batch.map((item) => ({
334
+ attributes: { type: 'FieldPermissions' },
335
+ Id: item.id,
336
+ PermissionsRead: true,
337
+ }));
338
+ try {
339
+ const results = await postCollection(instanceUrl, apiVersion, accessToken, 'PATCH', records);
340
+ results.forEach((result, idx) => {
341
+ const item = batch[idx];
342
+ if (result.success) {
343
+ granted.push({ sobject: item.sobject, field: item.field });
344
+ } else {
345
+ const error = (result.errors || []).map((e) => e.message).join('; ') || 'unknown error';
346
+ failed.push({ sobject: item.sobject, field: item.field, error });
347
+ }
348
+ });
349
+ } catch (error) {
350
+ batch.forEach((item) => failed.push({ sobject: item.sobject, field: item.field, error: error.message }));
351
+ }
352
+ }
353
+
354
+ const objectGranted = [];
355
+ const objectFailed = [];
356
+
357
+ for (const batch of chunk(toInsertObject, 200)) {
358
+ const records = batch.map((item) => {
359
+ const record = {
360
+ attributes: { type: 'ObjectPermissions' },
361
+ ParentId: permissionSetId,
362
+ SobjectType: item.sobject,
363
+ PermissionsRead: true,
364
+ PermissionsCreate: false,
365
+ PermissionsEdit: false,
366
+ PermissionsDelete: false,
367
+ PermissionsModifyAllRecords: false,
368
+ };
369
+ if (item.permissionsViewAllRecords) record.PermissionsViewAllRecords = true;
370
+ return record;
371
+ });
372
+ try {
373
+ const results = await postCollection(instanceUrl, apiVersion, accessToken, 'POST', records);
374
+ results.forEach((result, idx) => {
375
+ const item = batch[idx];
376
+ if (result.success) {
377
+ objectGranted.push({ sobject: item.sobject, permissionsRead: true, permissionsViewAllRecords: Boolean(item.permissionsViewAllRecords) });
378
+ } else {
379
+ const error = (result.errors || []).map((e) => e.message).join('; ') || 'unknown error';
380
+ objectFailed.push({ sobject: item.sobject, error });
381
+ }
382
+ });
383
+ } catch (error) {
384
+ batch.forEach((item) => objectFailed.push({ sobject: item.sobject, error: error.message }));
385
+ }
386
+ }
387
+
388
+ for (const batch of chunk(toUpdateObject, 200)) {
389
+ const records = batch.map((item) => {
390
+ const record = { attributes: { type: 'ObjectPermissions' }, Id: item.id };
391
+ if (item.permissionsRead) record.PermissionsRead = true;
392
+ if (item.permissionsViewAllRecords) record.PermissionsViewAllRecords = true;
393
+ return record;
394
+ });
395
+ try {
396
+ const results = await postCollection(instanceUrl, apiVersion, accessToken, 'PATCH', records);
397
+ results.forEach((result, idx) => {
398
+ const item = batch[idx];
399
+ if (result.success) {
400
+ objectGranted.push({
401
+ sobject: item.sobject,
402
+ permissionsRead: Boolean(item.permissionsRead),
403
+ permissionsViewAllRecords: Boolean(item.permissionsViewAllRecords),
404
+ });
405
+ } else {
406
+ const error = (result.errors || []).map((e) => e.message).join('; ') || 'unknown error';
407
+ objectFailed.push({ sobject: item.sobject, error });
408
+ }
409
+ });
410
+ } catch (error) {
411
+ batch.forEach((item) => objectFailed.push({ sobject: item.sobject, error: error.message }));
412
+ }
413
+ }
414
+
415
+ process.stdout.write(JSON.stringify({
416
+ permissionSetId,
417
+ permissionSetLabel,
418
+ dryRun: false,
419
+ alreadyGranted,
420
+ notFlsEligible,
421
+ granted,
422
+ failed,
423
+ objectAlreadyGranted,
424
+ objectGranted,
425
+ objectFailed,
426
+ }));
427
+ process.exit(failed.length || objectFailed.length ? 3 : 0);
428
+ }
429
+
430
+ main().catch((err) => {
431
+ console.error(err);
432
+ process.exit(1);
433
+ });