sf-forcekit 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/License +21 -0
- package/README.md +106 -0
- package/bin/cli.js +85 -0
- package/package.json +44 -0
- package/templates/ai-dir/README.md +155 -0
- package/templates/ai-dir/agentforce.md +213 -0
- package/templates/ai-dir/architecture.md +123 -0
- package/templates/ai-dir/commands.md +276 -0
- package/templates/ai-dir/context-snapshots/TEMPLATE.md +64 -0
- package/templates/ai-dir/conventions.md +242 -0
- package/templates/ai-dir/current-state.md +113 -0
- package/templates/ai-dir/debugging-notes.md +165 -0
- package/templates/ai-dir/deployment.md +161 -0
- package/templates/ai-dir/integrations.md +199 -0
- package/templates/ai-dir/inventory.md +209 -0
- package/templates/ai-dir/known-issues.md +124 -0
- package/templates/ai-dir/org-context.md +110 -0
- package/templates/ai-dir/performance.md +312 -0
- package/templates/ai-dir/prompts/agentforce.md +163 -0
- package/templates/ai-dir/prompts/apex.md +165 -0
- package/templates/ai-dir/prompts/flows.md +125 -0
- package/templates/ai-dir/prompts/lwc.md +230 -0
- package/templates/ai-dir/prompts/security.md +181 -0
- package/templates/ai-dir/prompts/testing.md +269 -0
- package/templates/ai-dir/rules.md +238 -0
- package/templates/ai-dir/scripts/update_state.py +1406 -0
- package/templates/ai-dir/source-of-truth.md +180 -0
- package/templates/ai-dir/templates/Selector.cls +113 -0
- package/templates/ai-dir/templates/Service.cls +132 -0
- package/templates/ai-dir/templates/TestClass.cls +143 -0
- package/templates/ai-dir/templates/TriggerHandler.cls +67 -0
- package/templates/ai-dir/testing-strategy.md +342 -0
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# Flow Building Rules
|
|
2
|
+
|
|
3
|
+
> Instructions for AI agents when designing or reviewing Salesforce Flows.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Core Rules
|
|
8
|
+
|
|
9
|
+
| Rule | Details |
|
|
10
|
+
|------|---------|
|
|
11
|
+
| **Prefer Record-Triggered Flows** | Over Process Builder (deprecated) and Workflow Rules |
|
|
12
|
+
| **Before-Save for field updates** | No DML consumed — free performance |
|
|
13
|
+
| **After-Save for related records** | Only when you need DML on other objects or platform events |
|
|
14
|
+
| **Fault paths on EVERY DML/callout** | No silent failures |
|
|
15
|
+
| **No hardcoded IDs** | Use Custom Labels or Custom Metadata Types |
|
|
16
|
+
| **No hardcoded strings** | Use Custom Labels for user-facing text |
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Flow Naming Convention
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
{Object}_{TriggerTiming}_{Purpose}
|
|
24
|
+
|
|
25
|
+
Examples:
|
|
26
|
+
Account_BeforeSave_DefaultIndustry
|
|
27
|
+
Contact_AfterSave_SyncToExternalSystem
|
|
28
|
+
Opportunity_BeforeSave_ValidateAmount
|
|
29
|
+
Case_AfterSave_NotifyTeam
|
|
30
|
+
Lead_Scheduled_CleanupStale
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
For non-record-triggered flows:
|
|
34
|
+
```
|
|
35
|
+
{Purpose}_{Type}
|
|
36
|
+
|
|
37
|
+
Examples:
|
|
38
|
+
OrderApproval_ScreenFlow
|
|
39
|
+
DataMigration_AutolaunchedFlow
|
|
40
|
+
WeeklyReport_ScheduledFlow
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## Before-Save vs After-Save Decision Tree
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
Need to update ONLY the triggering record's fields?
|
|
49
|
+
→ YES → Before-Save Flow (no DML cost)
|
|
50
|
+
→ NO → Do you need to:
|
|
51
|
+
• Create/update RELATED records? → After-Save Flow
|
|
52
|
+
• Send emails? → After-Save Flow
|
|
53
|
+
• Publish Platform Events? → After-Save Flow
|
|
54
|
+
• Make callouts? → After-Save Flow (+ Asynchronous path)
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## Mandatory Elements
|
|
60
|
+
|
|
61
|
+
### 1. Fault Paths
|
|
62
|
+
|
|
63
|
+
Every DML element (Create, Update, Delete) and every Action element (Callout, Invocable) MUST have a fault connector.
|
|
64
|
+
|
|
65
|
+
```
|
|
66
|
+
[Create Records] ──success──→ [Next Element]
|
|
67
|
+
│
|
|
68
|
+
└──fault──→ [Log Error] → [Notify Admin]
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### 2. Entry Conditions
|
|
72
|
+
|
|
73
|
+
- Be specific with entry conditions to avoid unnecessary executions
|
|
74
|
+
- Use `$Record.FieldName` for current values
|
|
75
|
+
- Use `$Record__Prior.FieldName` for before-update comparisons
|
|
76
|
+
- Filter with `AND`/`OR` to narrow scope
|
|
77
|
+
|
|
78
|
+
### 3. Bulkification
|
|
79
|
+
|
|
80
|
+
Flows are auto-bulkified for DML, but:
|
|
81
|
+
- Avoid **Get Records** inside loops (SOQL in loop equivalent)
|
|
82
|
+
- Use **Collection Variables** and **Assignment** elements for batch operations
|
|
83
|
+
- Pre-fetch related data before loops
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## Anti-Patterns (Avoid These)
|
|
88
|
+
|
|
89
|
+
| Anti-Pattern | Why It's Bad | Fix |
|
|
90
|
+
|-------------|-------------|-----|
|
|
91
|
+
| Get Records inside a Loop | SOQL in loop — hits governor limits | Pre-fetch outside loop into Collection Variable |
|
|
92
|
+
| Hardcoded Record Type IDs | Breaks across environments | Use `$Label.RecordTypeName` or CMDT |
|
|
93
|
+
| No fault paths | Silent failures, no debugging | Add fault connector to every DML/callout |
|
|
94
|
+
| After-Save for field updates | Wastes DML, causes recursion | Use Before-Save instead |
|
|
95
|
+
| Overly broad entry conditions | Runs on every update, burns CPU | Narrow with field change conditions |
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## Flow Testing Checklist
|
|
100
|
+
|
|
101
|
+
- [ ] Test with single record
|
|
102
|
+
- [ ] Test with bulk records (verify no governor limit issues)
|
|
103
|
+
- [ ] Test fault paths (what happens on DML error?)
|
|
104
|
+
- [ ] Test with different user profiles (sharing/FLS)
|
|
105
|
+
- [ ] Verify no recursion (update on same object doesn't re-trigger infinitely)
|
|
106
|
+
- [ ] Test entry conditions (verify flow doesn't fire when it shouldn't)
|
|
107
|
+
- [ ] Validate in sandbox before production
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
## Flow Documentation Template
|
|
112
|
+
|
|
113
|
+
When creating or reviewing a flow, document:
|
|
114
|
+
|
|
115
|
+
```markdown
|
|
116
|
+
### Flow: {Flow_Name}
|
|
117
|
+
- **Object:** {Object API Name}
|
|
118
|
+
- **Type:** Record-Triggered (Before/After Save) | Screen | Autolaunched | Scheduled
|
|
119
|
+
- **Entry Conditions:** {Describe when this flow fires}
|
|
120
|
+
- **Purpose:** {What this flow does}
|
|
121
|
+
- **DML Operations:** {List any create/update/delete}
|
|
122
|
+
- **Callouts:** {Any external callouts}
|
|
123
|
+
- **Platform Events:** {Any events published}
|
|
124
|
+
- **Error Handling:** {Fault path behavior}
|
|
125
|
+
```
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
# LWC Generation Rules
|
|
2
|
+
|
|
3
|
+
> Instructions for AI agents when generating Lightning Web Components.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Before Writing Any LWC
|
|
8
|
+
|
|
9
|
+
1. **Check `conventions.md`** for naming and styling rules.
|
|
10
|
+
2. **Check `architecture.md`** for existing components and patterns.
|
|
11
|
+
3. **Determine data strategy**: Wire Service → LDS → GraphQL → Imperative Apex (in order of preference).
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Component Structure
|
|
16
|
+
|
|
17
|
+
Every LWC must have this file structure:
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
lwc/
|
|
21
|
+
componentName/ ← camelCase, always
|
|
22
|
+
componentName.html ← Template
|
|
23
|
+
componentName.js ← Controller
|
|
24
|
+
componentName.css ← Styles (no inline styles)
|
|
25
|
+
componentName.js-meta.xml ← Metadata config
|
|
26
|
+
__tests__/ ← Jest tests
|
|
27
|
+
componentName.test.js
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## Mandatory Patterns
|
|
33
|
+
|
|
34
|
+
### 1. Three-State Template (Loading, Error, Data)
|
|
35
|
+
|
|
36
|
+
```html
|
|
37
|
+
<template>
|
|
38
|
+
<!-- Loading State -->
|
|
39
|
+
<template if:true={isLoading}>
|
|
40
|
+
<lightning-spinner
|
|
41
|
+
alternative-text="Loading"
|
|
42
|
+
size="medium">
|
|
43
|
+
</lightning-spinner>
|
|
44
|
+
</template>
|
|
45
|
+
|
|
46
|
+
<!-- Error State -->
|
|
47
|
+
<template if:true={hasError}>
|
|
48
|
+
<div class="slds-illustration slds-illustration_small">
|
|
49
|
+
<p class="slds-text-body_regular slds-text-color_error">
|
|
50
|
+
{errorMessage}
|
|
51
|
+
</p>
|
|
52
|
+
<lightning-button
|
|
53
|
+
label="Retry"
|
|
54
|
+
variant="brand"
|
|
55
|
+
onclick={handleRetry}>
|
|
56
|
+
</lightning-button>
|
|
57
|
+
</div>
|
|
58
|
+
</template>
|
|
59
|
+
|
|
60
|
+
<!-- Data State -->
|
|
61
|
+
<template if:true={hasData}>
|
|
62
|
+
<!-- Component content here -->
|
|
63
|
+
</template>
|
|
64
|
+
|
|
65
|
+
<!-- Empty State -->
|
|
66
|
+
<template if:false={hasData}>
|
|
67
|
+
<template if:false={isLoading}>
|
|
68
|
+
<template if:false={hasError}>
|
|
69
|
+
<div class="slds-align_absolute-center slds-p-around_medium">
|
|
70
|
+
<p class="slds-text-body_regular slds-text-color_weak">
|
|
71
|
+
No records found.
|
|
72
|
+
</p>
|
|
73
|
+
</div>
|
|
74
|
+
</template>
|
|
75
|
+
</template>
|
|
76
|
+
</template>
|
|
77
|
+
</template>
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### 2. Wire Service (Default for Data)
|
|
81
|
+
|
|
82
|
+
```javascript
|
|
83
|
+
import { LightningElement, api, wire } from 'lwc';
|
|
84
|
+
import getAccounts from '@salesforce/apex/AccountController.getAccounts';
|
|
85
|
+
|
|
86
|
+
export default class AccountList extends LightningElement {
|
|
87
|
+
@api recordId;
|
|
88
|
+
|
|
89
|
+
@wire(getAccounts, { accountId: '$recordId' })
|
|
90
|
+
wiredAccounts;
|
|
91
|
+
|
|
92
|
+
get isLoading() {
|
|
93
|
+
return !this.wiredAccounts.data && !this.wiredAccounts.error;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
get hasError() {
|
|
97
|
+
return !!this.wiredAccounts.error;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
get hasData() {
|
|
101
|
+
return this.wiredAccounts.data && this.wiredAccounts.data.length > 0;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
get errorMessage() {
|
|
105
|
+
return this.wiredAccounts.error?.body?.message || 'An error occurred';
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
get accounts() {
|
|
109
|
+
return this.wiredAccounts.data || [];
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### 3. Imperative Apex (Only When Wire Won't Work)
|
|
115
|
+
|
|
116
|
+
```javascript
|
|
117
|
+
import { LightningElement, api } from 'lwc';
|
|
118
|
+
import processRecords from '@salesforce/apex/RecordService.processRecords';
|
|
119
|
+
import { ShowToastEvent } from 'lightning/platformShowToastEvent';
|
|
120
|
+
|
|
121
|
+
export default class RecordProcessor extends LightningElement {
|
|
122
|
+
@api recordId;
|
|
123
|
+
isProcessing = false;
|
|
124
|
+
|
|
125
|
+
async handleProcess() {
|
|
126
|
+
this.isProcessing = true;
|
|
127
|
+
try {
|
|
128
|
+
const result = await processRecords({ recordId: this.recordId });
|
|
129
|
+
this.dispatchEvent(new ShowToastEvent({
|
|
130
|
+
title: 'Success',
|
|
131
|
+
message: result.message,
|
|
132
|
+
variant: 'success'
|
|
133
|
+
}));
|
|
134
|
+
} catch (error) {
|
|
135
|
+
this.dispatchEvent(new ShowToastEvent({
|
|
136
|
+
title: 'Error',
|
|
137
|
+
message: error.body?.message || 'Processing failed',
|
|
138
|
+
variant: 'error'
|
|
139
|
+
}));
|
|
140
|
+
} finally {
|
|
141
|
+
this.isProcessing = false;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### 4. CSS — No Inline Styles
|
|
148
|
+
|
|
149
|
+
```css
|
|
150
|
+
/* componentName.css */
|
|
151
|
+
|
|
152
|
+
/* Use SLDS design tokens via custom properties */
|
|
153
|
+
:host {
|
|
154
|
+
--card-spacing: var(--lwc-spacingMedium, 1rem);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
.container {
|
|
158
|
+
padding: var(--card-spacing);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
.highlight {
|
|
162
|
+
background-color: var(--lwc-colorBackgroundHighlight, #fffbe5);
|
|
163
|
+
border-radius: var(--lwc-borderRadiusMedium, 0.25rem);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/* ❌ NEVER do this in the template:
|
|
167
|
+
<div style="padding: 16px; color: red;"> */
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### 5. Meta XML Config
|
|
171
|
+
|
|
172
|
+
```xml
|
|
173
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
174
|
+
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
|
|
175
|
+
<apiVersion>66.0</apiVersion>
|
|
176
|
+
<isExposed>true</isExposed>
|
|
177
|
+
<targets>
|
|
178
|
+
<target>lightning__RecordPage</target>
|
|
179
|
+
<target>lightning__AppPage</target>
|
|
180
|
+
<target>lightning__HomePage</target>
|
|
181
|
+
</targets>
|
|
182
|
+
<targetConfigs>
|
|
183
|
+
<targetConfig targets="lightning__RecordPage">
|
|
184
|
+
<objects>
|
|
185
|
+
<object>Account</object>
|
|
186
|
+
</objects>
|
|
187
|
+
<property name="title" type="String" default="Account Summary" />
|
|
188
|
+
</targetConfig>
|
|
189
|
+
</targetConfigs>
|
|
190
|
+
</LightningComponentBundle>
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
---
|
|
194
|
+
|
|
195
|
+
## Event Communication
|
|
196
|
+
|
|
197
|
+
| Pattern | When | Direction |
|
|
198
|
+
|---------|------|-----------|
|
|
199
|
+
| `CustomEvent` | Parent-child | Child → Parent |
|
|
200
|
+
| `@api` property | Parent-child | Parent → Child |
|
|
201
|
+
| Lightning Message Service (LMS) | Cross-component (unrelated) | Any → Any |
|
|
202
|
+
| `pubsub` module | Legacy (avoid) | — |
|
|
203
|
+
|
|
204
|
+
```javascript
|
|
205
|
+
// Child: dispatch custom event
|
|
206
|
+
this.dispatchEvent(new CustomEvent('recordselected', {
|
|
207
|
+
detail: { recordId: this.selectedId },
|
|
208
|
+
bubbles: false, // Don't bubble by default
|
|
209
|
+
composed: false // Don't cross shadow DOM by default
|
|
210
|
+
}));
|
|
211
|
+
|
|
212
|
+
// Parent: listen in template
|
|
213
|
+
// <c-child-component onrecordselected={handleRecordSelected}></c-child-component>
|
|
214
|
+
handleRecordSelected(event) {
|
|
215
|
+
const selectedId = event.detail.recordId;
|
|
216
|
+
}
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
---
|
|
220
|
+
|
|
221
|
+
## Checklist Before Returning LWC Code
|
|
222
|
+
|
|
223
|
+
- [ ] camelCase component folder name
|
|
224
|
+
- [ ] Handles loading, error, and empty states
|
|
225
|
+
- [ ] Uses `@wire` (imperative only when necessary)
|
|
226
|
+
- [ ] No inline styles — CSS in component `.css` file
|
|
227
|
+
- [ ] Uses SLDS classes and design tokens
|
|
228
|
+
- [ ] Meta XML has correct API version (66.0) and targets
|
|
229
|
+
- [ ] Error messages displayed to user via toast or inline
|
|
230
|
+
- [ ] Event names are lowercase with no hyphens
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
# Security Rules
|
|
2
|
+
|
|
3
|
+
> Instructions for AI agents on security enforcement, CRUD/FLS, sharing, and AppExchange review readiness.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Non-Negotiable Security Rules
|
|
8
|
+
|
|
9
|
+
### 1. CRUD / FLS Enforcement
|
|
10
|
+
|
|
11
|
+
**Every SOQL query must enforce field-level security.**
|
|
12
|
+
|
|
13
|
+
```apex
|
|
14
|
+
// ✅ API v60.0+ — Use USER_MODE (preferred)
|
|
15
|
+
List<Account> accounts = [
|
|
16
|
+
SELECT Id, Name, Industry
|
|
17
|
+
FROM Account
|
|
18
|
+
WHERE Id IN :ids
|
|
19
|
+
WITH USER_MODE
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
// ✅ API v48.0+ — Alternative
|
|
23
|
+
List<Account> accounts = [
|
|
24
|
+
SELECT Id, Name, Industry
|
|
25
|
+
FROM Account
|
|
26
|
+
WHERE Id IN :ids
|
|
27
|
+
WITH SECURITY_ENFORCED
|
|
28
|
+
];
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
**DML with FLS enforcement:**
|
|
32
|
+
|
|
33
|
+
```apex
|
|
34
|
+
// Strip inaccessible fields before DML
|
|
35
|
+
SObjectAccessDecision decision = Security.stripInaccessible(
|
|
36
|
+
AccessType.CREATABLE, recordsToInsert
|
|
37
|
+
);
|
|
38
|
+
insert decision.getRecords();
|
|
39
|
+
|
|
40
|
+
// Check which fields were stripped
|
|
41
|
+
Map<String, Set<String>> removedFields = decision.getRemovedFields();
|
|
42
|
+
if (!removedFields.isEmpty()) {
|
|
43
|
+
Logger.warn('Fields stripped due to FLS: ' + removedFields);
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### 2. Sharing Model
|
|
48
|
+
|
|
49
|
+
```apex
|
|
50
|
+
// ✅ DEFAULT — Always use with sharing
|
|
51
|
+
public with sharing class AccountService { }
|
|
52
|
+
|
|
53
|
+
// ⚠️ EXCEPTION — Only with documented justification
|
|
54
|
+
// @reason: Needs cross-user visibility for admin batch job
|
|
55
|
+
public without sharing class AdminBatchService { }
|
|
56
|
+
|
|
57
|
+
// ❌ NEVER — Inherited sharing in new code
|
|
58
|
+
public class SomeService { } // Sharing context is unpredictable
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### 3. SOQL Injection Prevention
|
|
62
|
+
|
|
63
|
+
```apex
|
|
64
|
+
// ✅ CORRECT — Bind variables
|
|
65
|
+
String searchTerm = '%' + String.escapeSingleQuotes(userInput) + '%';
|
|
66
|
+
List<Account> results = [
|
|
67
|
+
SELECT Id, Name
|
|
68
|
+
FROM Account
|
|
69
|
+
WHERE Name LIKE :searchTerm
|
|
70
|
+
WITH USER_MODE
|
|
71
|
+
];
|
|
72
|
+
|
|
73
|
+
// ✅ For dynamic SOQL — Use Database.query with bind variables
|
|
74
|
+
String query = 'SELECT Id, Name FROM Account WHERE Industry = :industry WITH USER_MODE';
|
|
75
|
+
List<Account> results = Database.query(query);
|
|
76
|
+
|
|
77
|
+
// ❌ NEVER — String concatenation in SOQL
|
|
78
|
+
String query = 'SELECT Id FROM Account WHERE Name = \'' + userInput + '\''; // INJECTION!
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### 4. XSS Prevention (LWC/Aura)
|
|
82
|
+
|
|
83
|
+
```javascript
|
|
84
|
+
// ✅ LWC automatically escapes template expressions
|
|
85
|
+
// {accountName} in template is auto-escaped
|
|
86
|
+
|
|
87
|
+
// ❌ NEVER use lwc:dom="manual" to insert raw HTML from user input
|
|
88
|
+
// ❌ NEVER use innerHTML with unescaped user data
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### 5. No Hardcoded Secrets
|
|
92
|
+
|
|
93
|
+
```apex
|
|
94
|
+
// ❌ NEVER
|
|
95
|
+
String apiKey = 'sk-12345abcdef'; // Hardcoded credential!
|
|
96
|
+
String endpoint = 'https://api.external.com/v1'; // Hardcoded endpoint!
|
|
97
|
+
|
|
98
|
+
// ✅ ALWAYS — Named Credentials
|
|
99
|
+
HttpRequest req = new HttpRequest();
|
|
100
|
+
req.setEndpoint('callout:External_API/v1/resource');
|
|
101
|
+
// Auth header is automatically added by Named Credential
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## Security Review Checklist (Code Review)
|
|
107
|
+
|
|
108
|
+
Run this checklist on every PR:
|
|
109
|
+
|
|
110
|
+
1. ✅ **CRUD/FLS enforced** — `WITH USER_MODE` or `WITH SECURITY_ENFORCED` on all SOQL
|
|
111
|
+
2. ✅ **Sharing enforced** — `with sharing` on all classes (exceptions documented)
|
|
112
|
+
3. ✅ **No SOQL injection** — Bind variables, `String.escapeSingleQuotes()`
|
|
113
|
+
4. ✅ **No XSS** — No raw HTML insertion, template auto-escaping used
|
|
114
|
+
5. ✅ **No hardcoded credentials** — Named Credentials for all external auth
|
|
115
|
+
6. ✅ **No hardcoded IDs** — Custom Labels or Custom Metadata Types
|
|
116
|
+
7. ✅ **stripInaccessible** — Used before DML with external/untrusted data
|
|
117
|
+
8. ✅ **Error messages** — Don't expose internal stack traces to users
|
|
118
|
+
9. ✅ **Test coverage** — Permission-based tests with `System.runAs()`
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
## AppExchange Security Review Readiness
|
|
123
|
+
|
|
124
|
+
### Mandatory Scans
|
|
125
|
+
|
|
126
|
+
| Tool | What It Checks | Required? |
|
|
127
|
+
|------|---------------|-----------|
|
|
128
|
+
| **Salesforce Code Analyzer** | PMD rules, Apex best practices | ✅ Recommended |
|
|
129
|
+
| **Checkmarx** | SAST — Apex, VF, Lightning | ✅ Required for AppExchange |
|
|
130
|
+
| **OWASP ZAP / Burp Suite** | DAST — Runtime vulnerabilities | ✅ Required (Chimera retired June 2025) |
|
|
131
|
+
|
|
132
|
+
### Top Failure Reasons
|
|
133
|
+
|
|
134
|
+
1. **Missing CRUD/FLS** — Most common failure. Fix: `WITH USER_MODE` everywhere.
|
|
135
|
+
2. **SOQL Injection** — Dynamic queries without bind variables.
|
|
136
|
+
3. **XSS** — Unescaped output in Visualforce/Aura.
|
|
137
|
+
4. **Broken Access Control** — Missing `with sharing`, overly permissive profiles.
|
|
138
|
+
5. **Insecure Authentication** — Hardcoded credentials, no Named Credentials.
|
|
139
|
+
|
|
140
|
+
### 2026 Compliance Requirements
|
|
141
|
+
|
|
142
|
+
| Requirement | Deadline | Impact |
|
|
143
|
+
|------------|----------|--------|
|
|
144
|
+
| MFA enforcement | Mid-2026 | All users must have MFA enabled |
|
|
145
|
+
| Phishing-resistant MFA | Mid-2026 | Admin roles require hardware keys or passkeys |
|
|
146
|
+
| Login IP restrictions | Ongoing | Stricter enforcement on elevated profiles |
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
## Security Testing Template
|
|
151
|
+
|
|
152
|
+
```apex
|
|
153
|
+
@IsTest
|
|
154
|
+
private class SecurityTest {
|
|
155
|
+
|
|
156
|
+
@IsTest
|
|
157
|
+
static void testCrudFls_RestrictedUser() {
|
|
158
|
+
// Create user with minimum permissions
|
|
159
|
+
User restrictedUser = TestDataFactory.createStandardUser();
|
|
160
|
+
insert restrictedUser;
|
|
161
|
+
|
|
162
|
+
System.runAs(restrictedUser) {
|
|
163
|
+
Test.startTest();
|
|
164
|
+
|
|
165
|
+
// Verify FLS is enforced — user shouldn't see sensitive fields
|
|
166
|
+
try {
|
|
167
|
+
List<Account> accounts = AccountSelector.getByIds(
|
|
168
|
+
new Set<Id>{ /* test account IDs */ }
|
|
169
|
+
);
|
|
170
|
+
// Verify restricted fields are not accessible
|
|
171
|
+
} catch (System.QueryException e) {
|
|
172
|
+
// Expected: WITH USER_MODE blocks inaccessible fields
|
|
173
|
+
System.assert(e.getMessage().contains('insufficient access'),
|
|
174
|
+
'Should enforce FLS for restricted user');
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
Test.stopTest();
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
```
|