Skip to content

Add Adobe Marketo Engage gatekeeper - #394

Open
bjesus wants to merge 102 commits into
mainfrom
yoav/marketo-gatekeeper-public
Open

Add Adobe Marketo Engage gatekeeper#394
bjesus wants to merge 102 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

ask-bonk Bot commented Sep 1, 2026

Copy link
Copy Markdown

Findings

  1. [P1] marketo.ts:651-656,1155-1163: Observer admission can succeed after owner or observer credentials change during live verification. Recheck both credential generations before admitting.
  2. [P1] marketo-api.ts:1697: List-member reads use plural /v1/lists/{id}/leads.json; Adobe documents singular /v1/list/{id}/leads.json. All list membership reads fail.
  3. [P2] connect-ui.ts:147: The form defaults to GET. Without JavaScript, submission puts clientSecret in browser history and request logs. Set method="post".
  4. [P2] marketo-api.ts:740: A successful envelope missing result becomes an empty terminal page, silently truncating malformed responses instead of failing closed.
  5. [P2] session.ts:1632-1648: Dedupe-based custom-object deletion does not require every schema dedupe field, allowing underspecified, unapplicable destructive approvals.
  6. [P2] marketo.ts:1422-1429: Rejecting a metadata update does not block a later clone submitted from that simulated metadata, so the clone can use stale source state.
  7. [P2] design-studio.ts:849-854: Folder-scoped asset results validate only the parent ID, not its Folder/Program discriminator, allowing wrong-type rows with the same numeric ID.

github run

devin-ai-integration[bot]

This comment was marked as resolved.

ask-bonk[bot]

This comment was marked as resolved.

@ask-bonk

ask-bonk Bot commented Sep 1, 2026

Copy link
Copy Markdown

Posted 6 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 +1512 to +1516
this.ctx.storage.kv.put<PendingRow>(`pending:${id}`, {
action,
ownerGeneration: credentialState.generation,
});
this.ctx.storage.kv.put("pending:index", [...this.#pendingIndexIncludingBlocked(), id]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Immediate rejection leaves orphaned action

When approval is rejected before submitAction returns, the pending row is stored after rejection completes. It remains simulated and consumes capacity, but no approval can resolve it.

Prompt for agents
Rework Marketo action submission in packages/gatekeeper-marketo/src/marketo.ts so rejectAction cannot complete before the corresponding pending row exists. The current flow awaits ApprovalQueue.submitAction before persisting pending:<id> and pending:index, while rejectAction treats a missing row as a successful no-op. Stage the action before exposing the approval, as other gatekeepers do, and roll it back if submission fails. Preserve the current requirement that failed queue submission leaves no pending simulation, and add a concurrency test where rejection arrives while queue submission is in flight.
Devin Review

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

}

async addLeadsToList(listId: number, ids: number[]): Promise<RawSyncResult[]> {
return await this.#result<RawSyncResult>(`/v1/list/${listId}/leads.json`, {

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] Keep list mutations on the plural endpoint. Adobe defines Add to List and Remove from List as POST/DELETE on /rest/v1/lists/{listId}/leads.json; the singular endpoint is used for membership reads. Both approved mutations now call the wrong route and fail. Use the plural path here and at line 1715, and correct the test that pins the singular write path.

}

async removeObserver(id: string): Promise<void> {
await userAccountStub(this.ctx.exports, this.ctx.props.userObjectId).removeCollaborator(id);

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] Track observer admissions per binding. The overseer observer ID identifies a user within a gadget, so sibling Marketo bindings for the same account use the same ID. Their admissions overwrite the single account-level observer:${id} row, and removing one binding here deletes tracking needed by the others. If that collaborator remains on another binding and later revokes credentials, its admission can no longer tombstone the owner record, so observations omit excludeObservers. Store/refcount admissions by binding or admission ID and remove only this facet admission.

name: text(value.name, "Email name"), description: optionalText(value.description, "description"),
appData: { ...location(value.location, true, this.#ctx), editorType: "email" }, headers: headers(value.headers),
data: content(value.content), settings: settings(value.settings),
templateId: value.templateId === undefined ? undefined : id(value.templateId, "templateId", this.#ctx, "designerTemplate"),

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] Reject templateId together with content during creation too. Adobe documents that a create request with templateId overwrites any submitted data. This action currently records and simulates the caller content even though Marketo creates the template content instead, so the external asset differs from what was approved. Apply the same mutual-exclusion guard used by update().

if (!dependent || dependent.ownerGeneration !== pendingRow?.ownerGeneration) continue;
let row = dependent.action;
if (row.id <= pending.id ||
!isDesignStudioAction(row) && !isCampaignAction(row) && !isProgramAction(row) && !isEmailDesignerAction(row)) continue;

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] Invalidate sends derived from a rejected campaign rename. requestCampaign() and schedule() snapshot the simulated campaign name, but this rejection walk excludes campaignTrigger and campaignSchedule. Rejecting an earlier rename therefore leaves a real-send approval naming the rejected name, while dispatch targets the original campaign. Include these send actions in dependency blocking or revalidate the reviewed identity.

let snapshot: DesignStudioLifecycleSnapshot = {
metadata: lifecycleMetadata(metadata),
content: await this.lifecycleSnapshotContent(),
// Unlike Email Designer, the classic Asset API has no used-by endpoint.

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] Do not claim the classic API has no used-by endpoint. Adobe exposes paged used-by reads for at least email templates (/emailTemplates/{id}/usedBy.json) and forms (/form/{id}/usedBy.json). Hard-coding an empty set hides affected assets from approval and skips dependency revalidation before publish/delete. Populate the snapshot for supported asset types.

for (let reference of this.#actionReferences(action)) {
add(this.#referenceKey(reference), identity !== undefined && this.#sameReference(reference, identity));
}
return resources;

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] Order lifecycle propagation against affected dependents. A template or fragment approval can change inheriting emails, but designerLifecycle.affectedDependents never contributes resource keys here. The lifecycle action can therefore dispatch before an earlier pending mutation to one of those emails, reversing the submitted final state. Add the dependent email IDs as write conflicts, or serialize propagation conservatively.

if (!providerToken || providerToken.length > ACTIVITY_TOKEN_MAX_LENGTH) {
throw new MarketoError("Marketo returned an invalid static-list member page token.");
}
let bytes = new TextEncoder().encode(JSON.stringify({ version: 1, providerToken, scope }));

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] Authenticate the list paging scope. This is only base64-encoded caller-controlled JSON. A caller can take a provider token issued for list A, rewrite scope to list B, and pass it through the narrow list-B capability; member rows carry no list identity for response correlation. Keep cursors server-side or MAC the payload before treating its scope as an authorization boundary.

batchSize: query.maxResults,
});
let requestedTypeIds = new Set(query.activityTypeIds);
for (let activity of page.result) validateActivity(activity, requestedTypeIds, query.sinceDate);

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] Enforce the requested activity page bound. maxResults is validated and sent as batchSize, but this accepts every returned row without checking page.result.length. A malformed or misapplied response can make maxResults: 1 authorize and expose up to the provider maximum. Reject pages larger than the requested bound before observation.

@ask-bonk

ask-bonk Bot commented Sep 1, 2026

Copy link
Copy Markdown

Posted 8 actionable inline findings.

github run

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

2 participants