Skip to content

Add Adobe Marketo Engage gatekeeper - #394

Closed
bjesus wants to merge 127 commits into
mainfrom
yoav/marketo-gatekeeper-public
Closed

Add Adobe Marketo Engage gatekeeper#394
bjesus wants to merge 127 commits into
mainfrom
yoav/marketo-gatekeeper-public

Conversation

@bjesus

@bjesus bjesus commented Aug 31, 2026

Copy link
Copy Markdown
Member

Summary

  • Add an Adobe Marketo Engage gatekeeper using per-account LaunchPoint credentials
  • Support people, CRM objects, lists, programs, campaigns, activities, custom objects, and Design Studio assets
  • Provide granular instance, Design Studio, program, and static-list capabilities
  • Gate external writes through approval and simulate supported pending changes
  • Register Marketo in the public release manifest and connector documentation

Testing

  • pnpm exec vp run -F @gadgets/marketo-gatekeeper --no-cache build
  • pnpm exec vp run -F @gadgets/marketo-gatekeeper --no-cache test (239 tests)
  • node --test scripts/release/manifest-lib.test.ts
  • pnpm lint

pnpm test reaches an unrelated existing Google configurator test failure because URLPattern is unavailable in its Node environment.


Devin Review

@github-actions github-actions Bot added delivery Changes to CI or release delivery gatekeeper Changes to a gatekeeper integration labels Aug 31, 2026
github-advanced-security[bot]

This comment was marked as resolved.

@github-actions

Copy link
Copy Markdown

Preview: pr394-yoav-marketo-e4a63a94

https://pr394-yoav-marketo-e4a63a94-router.cloudflare-os-previews.workers.dev

Dashboard · deleted when this PR closes

devin-ai-integration[bot]

This comment was marked as resolved.

@ask-bonk

ask-bonk Bot commented Aug 31, 2026

Copy link
Copy Markdown

Findings

  1. High: marketo.ts:913 admits observers solely by stored credential equality. Revoked credentials or removed permissions still pass, exposing historical observations without current Marketo access. Perform a live access check or make sharing private-only.
  2. High: marketo.ts:1099 prevents rejecting any action with an applying:* state. Partial, uncertain, and known nothing-changed failures therefore remain permanently pending and cannot be discarded.
  3. High: marketo.ts:963 enforces ordering only for classic Design Studio clones. Approving a later publish/delete before an earlier update can publish stale content or delete the asset, diverging from the submitted sequence.
  4. Medium: config.ts:215 describes program grants as members, tokens, and statuses, but session.ts:759-815 also permits metadata changes, publishing, and permanent deletion. The connection UI understates the granted authority.
  5. Medium: marketo.ts:1132 removes dependent actions only from gatekeeper storage when rejecting a parent. Their ApprovalQueue entries remain pending and fail with “No queued Marketo action” if approved.
  6. Medium: session.ts:1243 calls the non-paged getLeads() implementation despite documenting that findPeople() returns all duplicates. Matches beyond Marketo’s first 300 results are silently lost.
  7. Medium: session.ts:1130 similarly exposes custom-object queries as complete arrays while marketo-api.ts:1857 discards nextPageToken, making records after the first page unreachable.
  8. Medium: session.ts:1151 promises custom-object deletion by dedupe or ID fields, but marketo-api.ts:1890 always uses deleteBy: "dedupeFields". Deleting with only marketoGUID fails or is skipped.
  9. Medium: email-designer.ts:304 prepends pending creations and truncates the provider page. A pending item displaces an upstream row that will not appear on the next provider-indexed page.
  10. Medium: email-designer.ts:295 ignores status, folder type, template, fragment, ownership, and sorting filters for pending assets, so filtered list calls return nonmatching simulated entries.

github run

devin-ai-integration[bot]

This comment was marked as resolved.

@ask-bonk

ask-bonk Bot commented Aug 31, 2026

Copy link
Copy Markdown

Findings

  1. High: session.ts:1042 bypasses pending campaign deletion when requesting or scheduling campaigns. These actions can be approved first and send real messages.
  2. Medium: marketo-api.ts:1457 comma-joins filter values. Literal commas split one value into several, returning unrelated people or objects.
  3. Medium: email-designer.ts:211 applies fragment settings under settings, while the returned fragment fields are top-level. Pending subtype/channel updates remain invisible.
  4. Medium: session.ts:1206-1208 disposes the shared approval queue even when returned child capabilities remain live, breaking subsequent child operations.
  5. Medium: design-studio.ts:325-337 simulates clones without inherited source metadata despite allowing callers to continue before approval.
  6. Medium: design-studio.ts:573-579 ignores maxDepth whenever an exact folder name is supplied, so the documented traversal constraint is not enforced.

github run

devin-ai-integration[bot]

This comment was marked as resolved.

@ask-bonk

ask-bonk Bot commented Aug 31, 2026

Copy link
Copy Markdown

Findings

  1. High: marketo.ts:1039 treats operation-less Email Designer parse errors as definitive. A successful create with a malformed response becomes retryable, causing duplicate assets.
  2. High: design-studio.ts:311 uses folderType as the Folder/Program discriminator. Adobe defines folderId.type for this; misclassified programs can incorrectly permit folder update/delete operations.
  3. Medium: session.ts:276 maps GUID-only activities to id: -1. Adobe is replacing numeric activity IDs with marketoGUID, so distinct activities become indistinguishable.
  4. Medium: email-designer-actions.ts:39 shows only the asset name for create approvals, hiding content, headers, settings, template, and destination from approvers.
  5. Medium: marketo-api.ts:371 does not notify credentialsExpired() when token acquisition fails before action dispatch, leaving revoked accounts displayed as connected.
  6. Medium: instance-configurator-ui.tsx:20 omits mutable Design Studio and standard CRM business-object access from the whole-instance consent description.
  7. Medium: marketo-api.ts:1581 and marketo-api.ts:1697 fetch only the first 200 tag types and channels despite offset pagination. Later channels are rejected as unknown, while required tags can be missed.
  8. Low: design-studio.ts:709 lets listFiles() accept a lifecycle status filter, but files have no status, so any such query returns an empty list.
  9. Low: design-studio.ts:1204 accepts updateContent({}), recording a successful approved action without making any Marketo request.

github run

ask-bonk[bot]

This comment was marked as resolved.

@ask-bonk

ask-bonk Bot commented Aug 31, 2026

Copy link
Copy Markdown

Posted 5 actionable inline findings.

github run

ask-bonk[bot]

This comment was marked as resolved.

@ask-bonk

ask-bonk Bot commented Aug 31, 2026

Copy link
Copy Markdown

Posted 5 actionable inline findings.

github run

devin-ai-integration[bot]

This comment was marked as resolved.

@ask-bonk

ask-bonk Bot commented Aug 31, 2026

Copy link
Copy Markdown

Findings

  1. High: session.ts:920 trusts a campaign returned by an exact-ID lookup without verifying its ID. A mismatched response can approve campaign A using campaign B’s name/requestability, then run A’s real messaging flow.
  2. Medium: marketo-api.ts:459 lets a failing credentialsExpired() callback replace a definitive authentication rejection. applyAction() then marks the definitely-unapplied write uncertain and unrejectable.
  3. Medium: marketo.ts:1102 cascades rejection only for classic Design Studio creates/clones. Rejecting a simulated content update leaves a dependent clone pending, which can then clone stale upstream content.
  4. Medium: actions.ts:281 does not revalidate campaign schedule time at dispatch. Delayed approval can place runAt inside Marketo’s five-minute minimum, causing failure or execution later than approved.
  5. Medium: token-cache.ts:49 refreshes continuously during a token’s final two minutes. Marketo returns the same token and remaining lifetime before expiry, so every request triggers another Identity call.

github run

@ask-bonk

ask-bonk Bot commented Aug 31, 2026

Copy link
Copy Markdown

Findings

  1. High: session.ts:360 trusts the first person lookup result without verifying the requested field/value. A mismatched response can expose, update, or delete another person.
  2. High: session.ts:326 returns person-scoped activities without validating each row’s leadId, potentially exposing another person’s activity history.
  3. High: actions.ts:339 does not correlate mutation result IDs with approved targets. For example, adding person 7 can return person 8 and still be marked applied.
  4. High: design-studio-actions.ts:203 and equivalent create handlers accept any plausible returned ID. A wrong response can bind provisional handles and subsequent writes to unrelated assets.
  5. High: marketo-api.ts:2778 models Adobe’s tag schema incorrectly. Adobe returns tagType, applicableProgramTypes, and required; allowable values require another endpoint. Consequently tag discovery and validation are broken.
  6. Medium: marketo.ts:1343 ignores earlier programStatus actions when ordering program deletion. Deletion can apply first, making the approved status change fail.
  7. Medium: email-designer.ts:209 replaces the entire simulated content object for partial updates. Updating only HTML makes untouched text content disappear from subsequent reads.
  8. Medium: business-object-actions.ts:89 omits written values and all targets after the tenth from approvals, contrary to ActionDescription’s complete-review requirement.
  9. Medium: actions.ts:90 similarly hides recipient IDs after the twentieth for campaign, list, and program-status actions, preventing review of the complete target set.

github run

ask-bonk[bot]

This comment was marked as resolved.

@ask-bonk

ask-bonk Bot commented Sep 1, 2026

Copy link
Copy Markdown

Posted 9 actionable inline findings.

github run

devin-ai-integration[bot]

This comment was marked as resolved.

@ask-bonk

ask-bonk Bot commented Sep 1, 2026

Copy link
Copy Markdown

Findings

  1. [P1] marketo.ts:917-935,1101-1113: Failed live observer re-verification leaves the previous admission generation valid. Existing sessions can continue receiving observations after Adobe revokes their credentials.

  2. [P1] design-studio.ts:1293-1299,1418-1422, marketo.ts:2804-2817: Classic lifecycle snapshots capture affected assets, but mutation ordering uses dependents only for Email Designer. Template/form publication can overtake earlier dependent-asset updates and reverse submitted ordering.

  3. [P2] session.ts:1632-1640,1699-1713: Custom-object schema reads can reveal dedupe-field metadata through errors without authorizeObservation(). The deletion path never authorizes this schema read.

  4. [P2] session.ts:1678-1681: createOrUpdate() queues custom-object records without requiring the schema’s complete dedupe fields. Adobe rejects such writes, producing approvals that cannot apply.

  5. [P2] email-designer.ts:641-674: Lifecycle preparation reads live state when content/settings/template updates are pending. Ordering applies the update first, after which lifecycle preflight rejects its stale snapshot, preventing valid update-then-approve batches.

  6. [P2] design-studio.ts:1293-1298: Lifecycle preparation unconditionally resolves a physical ID for dependency reads. Calling approve() on an unresolved template/snippet creation or clone fails despite the provisional handle’s simulated state.

  7. [P2] types.d.ts:1468-1480, design-studio-actions.ts:545-550: Forms expose deletion but no unapproval, although Adobe requires approved forms to be unapproved before deletion. Approved forms therefore cannot be deleted through this API.

  8. [P2] session.ts:1436-1452: Campaign activation verifies a trigger but not Adobe’s required flow step. Trigger-only campaigns receive approvals that Marketo will reject.

github run

if (isEmailDesignerAction(action) &&
(action.type === "designerLifecycle" || action.type === "designerDelete")) {
for (let dependent of action.affectedDependents) {
if (dependent.contentType === undefined || dependent.contentType.toLowerCase() === "email") {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Serialize Designer deletes against non-email dependents. This skips every captured dependent whose contentType is not email, even though the used-by response and tests include Smart Campaign dependents. If a campaign deletion/mutation is queued first and this Designer asset deletion second, applying the asset deletion first finds no shared resource key and can remove or fail on an asset while the campaign still references it, reversing the submitted order. Map supported dependent types to their action resource keys or conservatively serialize unknown dependents.

}

#businessObjectKeys(action: BusinessObjectAction): string[] {
let identities = [[BUSINESS_OBJECTS[action.kind].idField], BUSINESS_OBJECTS[action.kind].dedupeFields];

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Preserve ordering across business-object lookup aliases. Keys are derived only from identities present in each submitted payload, but neither matching mode requires the other identity. A dedupe-matched update {externalCompanyId: "acme", name: "first"} and a later ID-matched update {id: 7, name: "second"} can target the same company without sharing a key; approving the latter first leaves first, while submission order leaves second. The cross-strategy test currently supplies both aliases, masking normal valid inputs. Resolve aliases before queuing or serialize same-kind writes conservatively.

target: Record<string, unknown>,
snapshot: DesignerDeleteSnapshot,
): boolean {
return JSON.stringify(designerDeleteSnapshot(target)) === JSON.stringify(snapshot);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Exclude request-only settings from delete preflight snapshots. brandedDomain and dedicatedIp are deliberately stripped for create/update postflight because Marketo omits them from EmailSettingsResponseDTO, but an earlier pending update merges them into the delete snapshot and this exact comparison retains them. After update-then-delete is approved in order, the delete preflight can never match the conforming provider read, so the valid delete remains permanently retryable until rejected and resubmitted. Apply the same request-only normalization to delete snapshots/comparison.

if (page.result.some(record => !matches(record))) {
throw new MarketoError(`Marketo returned a ${this.kind} record outside the requested filter.`);
}
let idField = BUSINESS_OBJECTS[this.kind].idField;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Validate standard-object identities before authorizing records. idField is used only to shape a projection; rows are never required to contain a valid, unique identifier. For example, a successful { externalCompanyId: "acme" } row passes filter correlation and is authorized even though the public contract guarantees identifying fields and the record cannot be reliably correlated or targeted later. Validate positive numeric IDs/non-empty GUIDs and per-page uniqueness, analogous to validateCustomObjectRecords().

@ask-bonk

ask-bonk Bot commented Sep 1, 2026

Copy link
Copy Markdown

Posted 4 actionable inline findings.

github run

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 new potential issue.

1 flag not posted on this PR by your GitHub settings — view it in Devin Review. (Configure)

Devin Review

Comment on lines +2782 to +2784
if ((action.type === "campaignTrigger" ||
action.type === "campaignLifecycle" && action.operation === "activate") &&
(!Number.isSafeInteger(campaign.flowId) || campaign.flowId! <= 0)) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Invalid campaigns reach scheduling

#preflightCampaignOwnership omits schedule-time type and flow checks. A changed or empty campaign reaches Marketo and the approved schedule fails.

Prompt for agents
In packages/gatekeeper-marketo/src/marketo.ts, extend MarketoGatekeeperImpl.#preflightCampaignOwnership for campaignSchedule actions. Revalidate immediately before dispatch that the target remains a batch campaign and has a valid positive flowId, matching the submission-time checks in MarketoSmartCampaignImpl.schedule. Treat failures as DesignerPreDispatchError so no scheduling request is sent and the approval remains safely retryable.
Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

@ask-bonk

ask-bonk Bot commented Sep 1, 2026

Copy link
Copy Markdown

Findings

  1. [P1] types.d.ts:106-108, session.ts:1793-1807, marketo.ts:1769-1773: mutation records permit nested RPC stubs, which are then durably stored. Recursively restrict values to Marketo-compatible pass-by-value types.

  2. [P2] marketo.ts:549-569,622-630: interruption while awaiting the connection callback leaves reconnecting.generation stale. The alarm then refuses cleanup, leaving the account stuck reconnecting.

  3. [P2] marketo.ts:1769-1776: an isolate failure after staging but before submitAction() reaches the queue leaves an unresolvable staged action that permanently consumes pending capacity.

  4. [P2] email-designer.ts:731-739: dependency collection accepts a terminal 1,001st item. Dispatch later rejects the same approval at marketo.ts:2724, making it impossible to apply.

  5. [P2] email-designer.ts:439-450: list filters resolve provisional template IDs but forward folder/program IDs unchanged. Retained ~id folder handles therefore stop working after resolution.

  6. [P2] email-designer.ts:804-816: getUsedBy(0, 1) can return multiple records because result length is never checked against the requested page size.

github run

@bjesus

bjesus commented Sep 1, 2026

Copy link
Copy Markdown
Member Author

Superseded by #413, rebuilt from current main with a clean two-commit history and the retained security fixes.

@bjesus bjesus closed this Sep 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

delivery Changes to CI or release delivery gatekeeper Changes to a gatekeeper integration

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants