@pipedream/salesforce_rest_api 2.0.0 → 3.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.
Files changed (24) hide show
  1. package/actions/add-contact-to-campaign/add-contact-to-campaign.mjs +1 -1
  2. package/actions/add-lead-to-campaign/add-lead-to-campaign.mjs +1 -1
  3. package/actions/find-records/find-records.mjs +1 -1
  4. package/actions/list-case-comments/list-case-comments.mjs +1 -1
  5. package/actions/list-case-feed-items/list-case-feed-items.mjs +1 -1
  6. package/actions/list-cases/list-cases.mjs +1 -1
  7. package/actions/list-email-messages/list-email-messages.mjs +1 -1
  8. package/actions/list-email-templates/list-email-templates.mjs +1 -1
  9. package/actions/list-knowledge-articles/list-knowledge-articles.mjs +1 -1
  10. package/common/constants.mjs +5 -0
  11. package/package.json +1 -1
  12. package/sources/case-updated-instant/case-updated-instant.mjs +1 -1
  13. package/sources/common/common-feed.mjs +65 -6
  14. package/sources/email-template-updated-instant/email-template-updated-instant.mjs +1 -1
  15. package/sources/knowledge-article-updated-instant/knowledge-article-updated-instant.mjs +1 -1
  16. package/sources/new-case-instant/new-case-instant.mjs +1 -1
  17. package/sources/new-email-template-instant/new-email-template-instant.mjs +1 -1
  18. package/sources/new-feed-comment/new-feed-comment.mjs +3 -3
  19. package/sources/new-feed-item/new-feed-item.mjs +2 -2
  20. package/sources/new-knowledge-article-instant/new-knowledge-article-instant.mjs +1 -1
  21. package/sources/new-outbound-message/new-outbound-message.mjs +97 -18
  22. package/sources/new-record-instant/new-record-instant.mjs +1 -1
  23. package/sources/record-deleted-instant/record-deleted-instant.mjs +1 -1
  24. package/sources/record-updated-instant/record-updated-instant.mjs +1 -1
@@ -10,7 +10,7 @@ export default {
10
10
  + " The contact must already exist - use **Create Contact** if it does not."
11
11
  + " "
12
12
  + "[See the documentation](https://developer.salesforce.com/docs/atlas.en-us.228.0.object_reference.meta/object_reference/sforce_api_objects_campaignmember.htm)",
13
- version: "0.1.7",
13
+ version: "0.1.8",
14
14
  annotations: {
15
15
  destructiveHint: false,
16
16
  openWorldHint: true,
@@ -10,7 +10,7 @@ export default {
10
10
  + " The lead must already exist - use **Create Lead** if it does not."
11
11
  + " "
12
12
  + "[See the documentation](https://developer.salesforce.com/docs/atlas.en-us.228.0.object_reference.meta/object_reference/sforce_api_objects_campaignmember.htm)",
13
- version: "0.1.7",
13
+ version: "0.1.8",
14
14
  annotations: {
15
15
  destructiveHint: false,
16
16
  openWorldHint: true,
@@ -15,7 +15,7 @@ export default {
15
15
  + " Leaving `Record ID(s)` empty returns recent records, not every record - Salesforce sends one batch, so set `Limit` and use **SOQL Query** when you need everything."
16
16
  + " Newest-first ordering needs `CreatedDate`, so results are unordered on the few object types that lack it."
17
17
  + " [See the documentation](https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/resources_query.htm)",
18
- version: "0.3.1",
18
+ version: "0.3.2",
19
19
  annotations: {
20
20
  destructiveHint: false,
21
21
  openWorldHint: true,
@@ -13,7 +13,7 @@ export default {
13
13
  + " Find the case ID with **List Cases** first."
14
14
  + " For example, case ID `5005g00001ABCDeAAI` with `Limit` `20` returns that case's twenty most recent comments."
15
15
  + " [See the documentation](https://developer.salesforce.com/docs/atlas.en-us.object_reference.meta/object_reference/sforce_api_objects_casecomment.htm)",
16
- version: "0.1.1",
16
+ version: "0.1.2",
17
17
  annotations: {
18
18
  destructiveHint: false,
19
19
  openWorldHint: true,
@@ -12,7 +12,7 @@ export default {
12
12
  + " Use this to read a case's activity trail - text posts, status changes, logged calls, email events and case comment events - in one call."
13
13
  + " The case feed only exists when feed tracking is enabled for Cases in the Salesforce org, so an org without it returns no records."
14
14
  + " [See the documentation](https://developer.salesforce.com/docs/atlas.en-us.object_reference.meta/object_reference/sforce_api_associated_objects_feed.htm)",
15
- version: "0.0.2",
15
+ version: "0.0.3",
16
16
  annotations: {
17
17
  destructiveHint: false,
18
18
  openWorldHint: true,
@@ -14,7 +14,7 @@ export default {
14
14
  + " For example, `Status` `New` with `Limit` `10` returns the ten newest open cases."
15
15
  + " Status values are org-configurable."
16
16
  + " [See the documentation](https://developer.salesforce.com/docs/atlas.en-us.object_reference.meta/object_reference/sforce_api_objects_case.htm)",
17
- version: "0.0.2",
17
+ version: "0.0.3",
18
18
  annotations: {
19
19
  destructiveHint: false,
20
20
  openWorldHint: true,
@@ -13,7 +13,7 @@ export default {
13
13
  + " Find the case ID with **List Cases** first."
14
14
  + " Omit `Case ID` to list the most recent emails across the org, which can be large - set `Limit`."
15
15
  + " [See the documentation](https://developer.salesforce.com/docs/atlas.en-us.object_reference.meta/object_reference/sforce_api_objects_emailmessage.htm)",
16
- version: "0.1.1",
16
+ version: "0.1.2",
17
17
  annotations: {
18
18
  destructiveHint: false,
19
19
  openWorldHint: true,
@@ -12,7 +12,7 @@ export default {
12
12
  + " Use this to find a template and its ID before sending with **Send Email**, or before editing with **Update Email Template**."
13
13
  + " For example, run with `Limit` `50`, then pass the `Id` of the template you want to **Send Email**."
14
14
  + " [See the documentation](https://developer.salesforce.com/docs/atlas.en-us.object_reference.meta/object_reference/sforce_api_objects_emailtemplate.htm)",
15
- version: "0.1.1",
15
+ version: "0.1.2",
16
16
  annotations: {
17
17
  destructiveHint: false,
18
18
  openWorldHint: true,
@@ -12,7 +12,7 @@ export default {
12
12
  + " Returns the article container records (`KnowledgeArticle`), not the published article bodies - use **Get Knowledge Articles** to read article content, and **List Knowledge Data Category Groups** to discover the categories articles are filed under."
13
13
  + " This can return a lot of records on a mature org, so set `Limit`."
14
14
  + " [See the documentation](https://developer.salesforce.com/docs/atlas.en-us.object_reference.meta/object_reference/sforce_api_objects_knowledgearticle.htm)",
15
- version: "0.1.1",
15
+ version: "0.1.2",
16
16
  annotations: {
17
17
  destructiveHint: false,
18
18
  openWorldHint: true,
@@ -108,4 +108,9 @@ export default {
108
108
  "CreatedDate",
109
109
  ]),
110
110
  DEPLOY_HISTORICAL_LIMIT: 25,
111
+ // Salesforce IDs are delivered in an 18 character, case-insensitive form whose
112
+ // last 3 characters are a checksum over the case of the leading 15
113
+ ID_SHORT_LENGTH: 15,
114
+ // Minimum gap between re-resolving the connected org after a rejected message
115
+ ORGANIZATION_REFRESH_COOLDOWN_MS: 60000,
111
116
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pipedream/salesforce_rest_api",
3
- "version": "2.0.0",
3
+ "version": "3.0.0",
4
4
  "description": "Pipedream Salesforce (REST API) Components",
5
5
  "main": "salesforce_rest_api.app.mjs",
6
6
  "keywords": [
@@ -7,7 +7,7 @@ export default {
7
7
  name: "Case Updated (Instant, of Selectable Type)",
8
8
  key: "salesforce_rest_api-case-updated-instant",
9
9
  description: "Emit new event when a case is updated. [See the documentation](https://sforce.co/3yPSJZy)",
10
- version: "0.0.12",
10
+ version: "0.0.13",
11
11
  props: {
12
12
  salesforce: common.props.salesforce,
13
13
  db: "$.service.db",
@@ -26,7 +26,7 @@ export default {
26
26
  parentObjectType: {
27
27
  type: "string",
28
28
  label: "Parent Object Type",
29
- description: "Optional. The Salesforce SObject API name of the parent record to filter by, e.g. `Case` or `Opportunity`. When set, appends `AND Parent.Type = '<value>'` to the SOQL WHERE clause (traversal of the polymorphic `ParentId`). Leave blank to emit events on any parent object.",
29
+ description: "Optional. Only emit events whose parent record is of this Salesforce SObject API name, e.g. `Case` or `Opportunity`. Enforced on both delivery paths: `AND Parent.Type = '<value>'` in the polling SOQL, and the parent's Salesforce ID key prefix on instant (webhook) deliveries. Leave blank to emit events on any parent object.",
30
30
  optional: true,
31
31
  },
32
32
  excludeSelf: {
@@ -89,6 +89,62 @@ export default {
89
89
  }
90
90
  return userId;
91
91
  },
92
+ _getParentKeyPrefix() {
93
+ return this.db.get("parentKeyPrefix");
94
+ },
95
+ _setParentKeyPrefix(parentKeyPrefix) {
96
+ this.db.set("parentKeyPrefix", parentKeyPrefix);
97
+ },
98
+ async _resolveParentKeyPrefix() {
99
+ const objectType = this.parentObjectType;
100
+
101
+ // The cache records which object type it was resolved for, so editing the
102
+ // prop on a deployed source re-resolves instead of reusing a stale prefix.
103
+ const cached = this._getParentKeyPrefix();
104
+ if (cached?.objectType === objectType) {
105
+ return cached.keyPrefix;
106
+ }
107
+
108
+ let keyPrefix;
109
+ try {
110
+ ({ keyPrefix } = await this.getObjectTypeDescription(objectType));
111
+ } catch (err) {
112
+ console.log(`Error describing ${objectType} to resolve its ID key prefix:`, err);
113
+ }
114
+
115
+ if (!keyPrefix) {
116
+ // Not cached, so a transient describe failure is retried on the next event.
117
+ console.log(`No ID key prefix available for ${objectType}, falling back to a per-event Parent.Type lookup.`);
118
+ return null;
119
+ }
120
+
121
+ this._setParentKeyPrefix({
122
+ objectType,
123
+ keyPrefix,
124
+ });
125
+ return keyPrefix;
126
+ },
127
+ async _parentTypeMatches(record) {
128
+ const parentId = record?.ParentId;
129
+ if (!parentId) {
130
+ return false;
131
+ }
132
+
133
+ // Salesforce ID key prefixes are stable per SObject (Case = `500`,
134
+ // Account = `001`, ...), so the parent's type is read straight off the
135
+ // pushed ParentId with no extra API call per event.
136
+ const keyPrefix = await this._resolveParentKeyPrefix();
137
+ if (keyPrefix) {
138
+ return parentId.startsWith(keyPrefix);
139
+ }
140
+
141
+ // Fallback for an object with no key prefix: one SOQL lookup per event,
142
+ // reusing the same polymorphic traversal as the polling query.
143
+ const { records } = await this.query({
144
+ query: `SELECT Id FROM ${this.getObjectType()} WHERE Id = '${record.Id}' AND Parent.Type = '${this.parentObjectType}'`,
145
+ });
146
+ return !!records?.length;
147
+ },
92
148
  async _buildExtraConditions() {
93
149
  const conditions = [];
94
150
  if (this.parentObjectType) {
@@ -101,13 +157,16 @@ export default {
101
157
  return conditions.join(" ");
102
158
  },
103
159
  async processWebhookEvent(event) {
104
- // Instant/webhook deliveries can't filter on Parent.Type (the pushed
105
- // payload has ParentId but not the parent's object type), so
106
- // parentObjectType is only enforced on the polling/deploy SOQL queries.
107
- // excludeSelf is enforced here to prevent self-trigger loops.
160
+ // Instant/webhook deliveries can't filter on Parent.Type in SOQL (the
161
+ // pushed payload carries ParentId but not the parent's object type), so
162
+ // both props are enforced here to match the polling/deploy queries.
163
+ const record = event.body?.New;
164
+ if (this.parentObjectType && !(await this._parentTypeMatches(record))) {
165
+ return;
166
+ }
108
167
  if (this.excludeSelf) {
109
168
  const userId = await this._resolveAuthenticatedUserId();
110
- if (event.body?.New?.CreatedById === userId) {
169
+ if (record?.CreatedById === userId) {
111
170
  return;
112
171
  }
113
172
  }
@@ -7,7 +7,7 @@ export default {
7
7
  name: "Email Template Updated (Instant, of Selectable Type)",
8
8
  key: "salesforce_rest_api-email-template-updated-instant",
9
9
  description: "Emit new event when an email template is updated. [See the documentation](https://sforce.co/3yPSJZy)",
10
- version: "0.0.12",
10
+ version: "0.0.13",
11
11
  props: {
12
12
  salesforce: common.props.salesforce,
13
13
  db: "$.service.db",
@@ -7,7 +7,7 @@ export default {
7
7
  name: "Knowledge Article Updated (Instant, of Selectable Type)",
8
8
  key: "salesforce_rest_api-knowledge-article-updated-instant",
9
9
  description: "Emit new event when a knowledge article is updated. [See the documentation](https://sforce.co/3yPSJZy)",
10
- version: "0.0.12",
10
+ version: "0.0.13",
11
11
  props: {
12
12
  salesforce: common.props.salesforce,
13
13
  db: "$.service.db",
@@ -7,7 +7,7 @@ export default {
7
7
  name: "New Case (Instant, of Selectable Type)",
8
8
  key: "salesforce_rest_api-new-case-instant",
9
9
  description: "Emit new event when a case is created. [See the documentation](https://sforce.co/3yPSJZy)",
10
- version: "0.0.12",
10
+ version: "0.0.13",
11
11
  props: {
12
12
  salesforce: common.props.salesforce,
13
13
  db: "$.service.db",
@@ -7,7 +7,7 @@ export default {
7
7
  name: "New Email Template (Instant, of Selectable Type)",
8
8
  key: "salesforce_rest_api-new-email-template-instant",
9
9
  description: "Emit new event when an email template is created. [See the documentation](https://sforce.co/3yPSJZy)",
10
- version: "0.0.12",
10
+ version: "0.0.13",
11
11
  props: {
12
12
  salesforce: common.props.salesforce,
13
13
  db: "$.service.db",
@@ -11,13 +11,13 @@ export default {
11
11
  type: "source",
12
12
  name: "New Chatter Feed Comment (Instant or Polling)",
13
13
  key: "salesforce_rest_api-new-feed-comment",
14
- description: "Emit new events for each Chatter FeedComment (reply) created in Salesforce, polling `FeedComment` via SOQL on `CreatedDate`. Use this to react to comments on Chatter posts, since Chatter activity does not update the parent record's `LastModifiedDate`. The payload includes both `ParentId` (a polymorphic reference to the feed's parent - either a record feed, e.g. a Case ID starting with `500`, or a User feed) and `FeedItemId` (the ID of the FeedItem the comment belongs to) - these are distinct fields; do not confuse them. Set `parentObjectType` to a parent object API name (e.g. `Case`) to append `AND Parent.Type = '<value>'` to the SOQL WHERE clause. Set `excludeSelf` to `true` to drop comments authored by the connected integration user. Note: querying FeedComment without a parent filter requires the `View All Data` permission on the connected user. Attempts instant delivery via webhook and falls back to timer polling automatically. [See the documentation](https://developer.salesforce.com/docs/atlas.en-us.object_reference.meta/object_reference/sforce_api_objects_feedcomment.htm)",
15
- version: "0.0.4",
14
+ description: "Emit new events for each Chatter FeedComment (reply) created in Salesforce, polling `FeedComment` via SOQL on `CreatedDate`. Use this to react to comments on Chatter posts, since Chatter activity does not update the parent record's `LastModifiedDate`. The payload includes both `ParentId` (a polymorphic reference to the feed's parent - either a record feed, e.g. a Case ID starting with `500`, or a User feed) and `FeedItemId` (the ID of the FeedItem the comment belongs to) - these are distinct fields; do not confuse them. Set `parentObjectType` to a parent object API name (e.g. `Case`) to only emit comments whose parent record is of that type. Set `excludeSelf` to `true` to drop comments authored by the connected integration user. Note: querying FeedComment without a parent filter requires the `View All Data` permission on the connected user. Attempts instant delivery via webhook and falls back to timer polling automatically. [See the documentation](https://developer.salesforce.com/docs/atlas.en-us.object_reference.meta/object_reference/sforce_api_objects_feedcomment.htm)",
15
+ version: "0.0.6",
16
16
  props: {
17
17
  ...common.props,
18
18
  parentObjectType: {
19
19
  ...common.props.parentObjectType,
20
- description: "Optional. The Salesforce SObject API name of the parent business record to filter by, e.g. `Case`. When set, appends `AND Parent.Type = '<value>'` to the SOQL WHERE clause (traversal of the polymorphic `FeedComment.ParentId`). Leave blank to emit comments on any parent object (requires `View All Data` permission).",
20
+ description: "Optional. Only emit comments whose parent record is of this Salesforce SObject API name, e.g. `Case` (traversal of the polymorphic `FeedComment.ParentId`). Enforced on both delivery paths: `AND Parent.Type = '<value>'` in the polling SOQL, and the parent's Salesforce ID key prefix on instant (webhook) deliveries. Leave blank to emit comments on any parent object (requires `View All Data` permission).",
21
21
  },
22
22
  excludeSelf: {
23
23
  ...common.props.excludeSelf,
@@ -12,12 +12,12 @@ export default {
12
12
  name: "New Chatter Feed Item (Instant or Polling)",
13
13
  key: "salesforce_rest_api-new-feed-item",
14
14
  description: "Emit new events for each Chatter FeedItem (post) created in Salesforce, polling `FeedItem` via SOQL on `CreatedDate`. Use this to react to Chatter posts on Cases and other records, since Chatter activity does not update the parent record's `LastModifiedDate` (so **New Record (Instant, of Selectable Type)** and **New Case (Instant, of Selectable Type)** never emit for it). Set `parentObjectType` to the parent object's API name (e.g. `Case`, `Opportunity`) to only emit posts whose parent record is of that type. Set `excludeSelf` to `true` to drop posts authored by the connected integration user. Note: `Body` is null for system-generated post types (e.g. `TrackedChange`). Attempts instant delivery via webhook and falls back to timer polling automatically when the Streaming API does not support this object. [See the documentation](https://developer.salesforce.com/docs/atlas.en-us.object_reference.meta/object_reference/sforce_api_objects_feeditem.htm)",
15
- version: "0.0.4",
15
+ version: "0.0.6",
16
16
  props: {
17
17
  ...common.props,
18
18
  parentObjectType: {
19
19
  ...common.props.parentObjectType,
20
- description: "Optional. The Salesforce SObject API name of the parent record to filter by, e.g. `Case` or `Opportunity`. When set, appends `AND Parent.Type = '<value>'` to the SOQL WHERE clause (traversal of the polymorphic `FeedItem.ParentId`). Leave blank to emit posts on any parent object.",
20
+ description: "Optional. Only emit posts whose parent record is of this Salesforce SObject API name, e.g. `Case` or `Opportunity` (traversal of the polymorphic `FeedItem.ParentId`). Enforced on both delivery paths: `AND Parent.Type = '<value>'` in the polling SOQL, and the parent's Salesforce ID key prefix on instant (webhook) deliveries. Leave blank to emit posts on any parent object.",
21
21
  },
22
22
  excludeSelf: {
23
23
  ...common.props.excludeSelf,
@@ -7,7 +7,7 @@ export default {
7
7
  name: "New Knowledge Article (Instant, of Selectable Type)",
8
8
  key: "salesforce_rest_api-new-knowledge-article-instant",
9
9
  description: "Emit new event when a knowledge article is created. [See the documentation](https://sforce.co/3yPSJZy)",
10
- version: "0.0.12",
10
+ version: "0.0.13",
11
11
  props: {
12
12
  salesforce: common.props.salesforce,
13
13
  db: "$.service.db",
@@ -1,12 +1,13 @@
1
1
  import { XMLParser } from "fast-xml-parser";
2
2
  import salesforce from "../../salesforce_rest_api.app.mjs";
3
+ import constants from "../../common/constants.mjs";
3
4
 
4
5
  export default {
5
6
  type: "source",
6
7
  name: "New Outbound Message (Instant)",
7
8
  key: "salesforce_rest_api-new-outbound-message",
8
- description: "Emit new event when a new outbound message is received in Salesforce.",
9
- version: "0.1.14",
9
+ description: "Emit new event when a new outbound message is received in Salesforce. [See the documentation](https://developer.salesforce.com/docs/atlas.en-us.api.meta/api/sforce_api_om_outboundmessaging_notifications.htm)",
10
+ version: "1.0.0",
10
11
  dedupe: "unique",
11
12
  props: {
12
13
  db: "$.service.db",
@@ -15,6 +16,13 @@ export default {
15
16
  customResponse: true,
16
17
  },
17
18
  salesforce,
19
+ validateSessionId: {
20
+ type: "boolean",
21
+ label: "Validate Session ID",
22
+ description: "Additionally verify the `SessionId` of each outbound message against the Salesforce API. Requires the outbound message's `Send Session ID` option to be enabled. Leave this disabled unless you need it: sessions that are IP-locked, or that belong to a user without API access, cannot be verified and their messages are rejected.",
23
+ optional: true,
24
+ default: false,
25
+ },
18
26
  infoBox: {
19
27
  type: "alert",
20
28
  alertType: "info",
@@ -22,13 +30,33 @@ export default {
22
30
  \\
23
31
  Set the Outbound Message's \`Endpoint URL\` to the endpoint of this source, which you can view after it is created.
24
32
  \\
25
- The \`Send Session ID\` option must be enabled in Salesforce for validating outbound messages.`,
33
+ The outbound message must be sent from the same Salesforce org as the connected account above. Each message is verified against that org's \`Organization Id\`, so the \`Send Session ID\` option is no longer required.`,
26
34
  },
27
35
  },
28
36
  methods: {
37
+ _getOrganizationId() {
38
+ return this.db.get("organizationId");
39
+ },
40
+ _setOrganizationId(organizationId) {
41
+ this.db.set("organizationId", organizationId);
42
+ },
43
+ _getOrganizationCheckedAt() {
44
+ return this.db.get("organizationCheckedAt") || 0;
45
+ },
46
+ _setOrganizationCheckedAt(timestamp) {
47
+ this.db.set("organizationCheckedAt", timestamp);
48
+ },
29
49
  _unwrapMessage(message) {
30
50
  const parser = new XMLParser({
31
51
  removeNSPrefix: true,
52
+ // Salesforce sends identifiers such as CaseNumber, postal codes and
53
+ // external IDs as digit strings. Parsing those as numbers strips
54
+ // leading zeros and rewrites E-notation and hex-like values.
55
+ numberParseOptions: {
56
+ leadingZeros: false,
57
+ eNotation: false,
58
+ hex: false,
59
+ },
32
60
  });
33
61
  const obj = parser.parse(message);
34
62
  const notifications = obj["Envelope"]["Body"].notifications;
@@ -55,22 +83,69 @@ The \`Send Session ID\` option must be enabled in Salesforce for validating outb
55
83
  },
56
84
  });
57
85
  },
58
- async _isValidSessionId(sessionId) {
59
- try {
60
- const data = await this.salesforce.getUserInfo(sessionId);
61
- return Boolean(data);
62
- } catch (err) {
63
- console.log("Error validating SessionId:", err);
86
+ // Compare on the leading 15 characters so that a 15 character ID and its 18
87
+ // character form match. Both values come from Salesforce in canonical case,
88
+ // so the comparison stays case-sensitive.
89
+ _toShortId(id) {
90
+ return `${id}`.slice(0, constants.ID_SHORT_LENGTH);
91
+ },
92
+ async _fetchOrganizationId() {
93
+ const { organization_id: organizationId } =
94
+ await this.salesforce.getUserInfo(this.salesforce._authToken());
95
+ return organizationId;
96
+ },
97
+ // The cached ID belongs to whichever account was connected when it was
98
+ // stored, and Salesforce exposes no stable connection identity to key it
99
+ // on. So re-resolve from the connected account whenever the incoming ID
100
+ // does not match: pointing the source at a different org then heals itself
101
+ // on the next message instead of rejecting every message from then on.
102
+ async _matchesConnectedOrganization(organizationId) {
103
+ const cachedOrganizationId = this._getOrganizationId();
104
+ if (cachedOrganizationId
105
+ && this._toShortId(organizationId) === this._toShortId(cachedOrganizationId)) {
106
+ return true;
107
+ }
108
+
109
+ // Re-resolve on a mismatch, but at most once per cooldown, so a stream of
110
+ // rejected messages cannot amplify into a stream of Salesforce API calls.
111
+ const now = Date.now();
112
+ if (now - this._getOrganizationCheckedAt() < constants.ORGANIZATION_REFRESH_COOLDOWN_MS) {
64
113
  return false;
65
114
  }
115
+
116
+ // Stamp before the request so a failing or concurrent refresh still
117
+ // consumes the cooldown rather than retrying on every delivery.
118
+ this._setOrganizationCheckedAt(now);
119
+ const currentOrganizationId = await this._fetchOrganizationId();
120
+ this._setOrganizationId(currentOrganizationId);
121
+ return this._toShortId(organizationId) === this._toShortId(currentOrganizationId);
122
+ },
123
+ // Errors are intentionally not caught: a session that cannot be verified
124
+ // must surface as a failed execution rather than a discarded message.
125
+ async _isValidSessionId(sessionId) {
126
+ const data = await this.salesforce.getUserInfo(sessionId);
127
+ return Boolean(data);
66
128
  },
67
129
  async _isValidSource(data) {
68
- const { SessionId: sessionId } = data;
69
- if (!sessionId) {
70
- throw new Error("The outbound message is missing a Session ID. Please configure the outbound message to send Session ID to validate the webhook source.");
130
+ const {
131
+ OrganizationId: organizationId,
132
+ SessionId: sessionId,
133
+ } = data;
134
+ if (!organizationId) {
135
+ return false;
136
+ }
137
+
138
+ if (!(await this._matchesConnectedOrganization(organizationId))) {
139
+ return false;
71
140
  }
72
141
 
73
- return this._isValidSessionId(sessionId);
142
+ if (!this.validateSessionId) {
143
+ return true;
144
+ }
145
+
146
+ return sessionId
147
+ ? this._isValidSessionId(sessionId)
148
+ : false;
74
149
  },
75
150
  generateMeta(data) {
76
151
  const {
@@ -78,7 +153,7 @@ The \`Send Session ID\` option must be enabled in Salesforce for validating outb
78
153
  Notification: { Id: eventId },
79
154
  } = data;
80
155
  const id = `${eventId}-${actionId}`;
81
- const summary = JSON.stringify(data);
156
+ const summary = `New outbound message with ID ${eventId}`;
82
157
  const ts = Date.now();
83
158
  return {
84
159
  id,
@@ -89,13 +164,17 @@ The \`Send Session ID\` option must be enabled in Salesforce for validating outb
89
164
  },
90
165
  async run(event) {
91
166
  const { bodyRaw } = event;
92
- this._sendHttpResponse(true);
93
167
  const data = this._unwrapMessage(bodyRaw);
168
+
94
169
  if (!(await this._isValidSource(data))) {
95
- console.log("Skipping event from unrecognized source");
96
- return;
170
+ // Acknowledge `false` so Salesforce keeps the message queued and retries
171
+ // for up to 24 hours, rather than dropping it as delivered.
172
+ this._sendHttpResponse(false);
173
+ throw new Error(`Rejected an outbound message from Salesforce organization \`${data.OrganizationId}\` (connected organization: \`${this._getOrganizationId() || "unknown"}\`). Check that this source is connected to the organization that sends the outbound message, and, if \`Validate Session ID\` is enabled, that the message includes a valid Session ID.`);
97
174
  }
98
175
 
176
+ this._sendHttpResponse(true);
177
+
99
178
  let notifications = data.Notification;
100
179
  if (!Array.isArray(notifications)) {
101
180
  notifications = [
@@ -104,7 +183,7 @@ The \`Send Session ID\` option must be enabled in Salesforce for validating outb
104
183
  }
105
184
 
106
185
  notifications.forEach((n) => {
107
- const notification = Object.assign(data, {
186
+ const notification = Object.assign({}, data, {
108
187
  Notification: n,
109
188
  });
110
189
  this.$emit(notification, this.generateMeta(notification));
@@ -6,7 +6,7 @@ export default {
6
6
  name: "New Record (Instant, of Selectable Type)",
7
7
  key: "salesforce_rest_api-new-record-instant",
8
8
  description: "Emit new event when a record of the selected object type is created. [See the documentation](https://sforce.co/3yPSJZy)",
9
- version: "0.2.12",
9
+ version: "0.2.13",
10
10
  props: {
11
11
  ...common.props,
12
12
  fieldsToObtain: {
@@ -7,7 +7,7 @@ export default {
7
7
  name: "New Deleted Record (Instant, of Selectable Type)",
8
8
  key: "salesforce_rest_api-record-deleted-instant",
9
9
  description: "Emit new event when a record of the selected object type is deleted. [See the documentation](https://sforce.co/3msDDEE)",
10
- version: "0.1.11",
10
+ version: "0.1.12",
11
11
  methods: {
12
12
  ...common.methods,
13
13
  generateWebhookMeta(data) {
@@ -7,7 +7,7 @@ export default {
7
7
  name: "New Updated Record (Instant, of Selectable Type)",
8
8
  key: "salesforce_rest_api-record-updated-instant",
9
9
  description: "Emit new event when a record of the selected type is updated. [See the documentation](https://sforce.co/3yPSJZy)",
10
- version: "0.2.12",
10
+ version: "0.2.13",
11
11
  props: {
12
12
  ...common.props,
13
13
  fields: {