Add Adobe Marketo Engage gatekeeper - #394
Conversation
Preview:
|
|
Findings
|
|
Findings
|
|
Findings
|
|
Posted 5 actionable inline findings. |
|
Posted 5 actionable inline findings. |
|
Findings
|
|
Findings
|
|
Findings
|
|
Posted 6 actionable inline findings. |
There was a problem hiding this comment.
Devin Review found 1 new potential issue.
1 flag not posted on this PR by your GitHub settings — view it in Devin Review. (Configure)
| this.ctx.storage.kv.put<PendingRow>(`pending:${id}`, { | ||
| action, | ||
| ownerGeneration: credentialState.generation, | ||
| }); | ||
| this.ctx.storage.kv.put("pending:index", [...this.#pendingIndexIncludingBlocked(), id]); |
There was a problem hiding this comment.
🟡 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.
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`, { |
There was a problem hiding this comment.
[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); |
There was a problem hiding this comment.
[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"), |
There was a problem hiding this comment.
[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; |
There was a problem hiding this comment.
[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. |
There was a problem hiding this comment.
[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; |
There was a problem hiding this comment.
[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 })); |
There was a problem hiding this comment.
[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); |
There was a problem hiding this comment.
[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.
|
Posted 8 actionable inline findings. |
Summary
Testing
pnpm exec vp run -F @gadgets/marketo-gatekeeper --no-cache buildpnpm exec vp run -F @gadgets/marketo-gatekeeper --no-cache test(239 tests)node --test scripts/release/manifest-lib.test.tspnpm lintpnpm testreaches an unrelated existing Google configurator test failure becauseURLPatternis unavailable in its Node environment.