feat(granola): complete API coverage, note triggers, and connector validation - #6880
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
PR SummaryCursor Bugbot is generating a summary for commit 214d699. Configure here. |
Greptile SummaryThe PR expands Granola API, trigger, and connector support while correcting output schemas, transcript handling, pagination completeness, and webhook lifecycle validation.
Confidence Score: 4/5The PR is not yet safe to merge because endpoint registration can still create duplicate active Granola endpoints when execution stops after the external POST but before its ID is checkpointed. Granola endpoint creation remains a non-idempotent external side effect separated from persistence of the returned ownership state, leaving the previously reported duplicate-registration failure outstanding. Files Needing Attention: apps/sim/lib/webhooks/providers/granola.ts
|
| Filename | Overview |
|---|---|
| apps/sim/lib/webhooks/providers/granola.ts | Adds signature verification and managed subscription lifecycle, but endpoint creation remains non-idempotent across the external-create/checkpoint boundary. |
| apps/sim/lib/webhooks/providers/granola.test.ts | Covers authentication, event matching, normalization, idempotency extraction, and endpoint cleanup behavior. |
| apps/sim/triggers/granola/webhook.ts | Defines the all-note-events trigger and its normalized output contract. |
| apps/sim/connectors/granola/granola.ts | Preserves Granola's truncation signal and aligns declared MIME type with stored plain-text content. |
| apps/sim/blocks/blocks/granola.ts | Exposes the expanded Granola tool and trigger surfaces with parameter coercion and matching block contracts. |
| apps/sim/tools/granola/create_webhook_endpoint.ts | Adds direct webhook-endpoint creation and maps the one-time signing secret into the documented tool result. |
Sequence Diagram
sequenceDiagram
participant Deploy as Deployment
participant Sim as Sim webhook registration
participant Granola as Granola API
participant Store as Registration store
Deploy->>Sim: Prepare trigger subscription
Sim->>Granola: POST webhook endpoint
Granola-->>Sim: Endpoint ID and signing secret
Sim->>Store: Checkpoint provider configuration
Granola->>Sim: Signed note event
Sim->>Sim: Verify signature and deduplicate event_id
Sim->>Deploy: Start workflow execution
Deploy->>Sim: Undeploy trigger
Sim->>Granola: DELETE endpoint by stored ID
Reviews (4): Last reviewed commit: "fix(granola): never recover an orphaned ..." | Re-trigger Greptile
…ixes
Granola's public API exposes nine endpoints; Sim implemented three. Adds the
remaining six and wires the new programmatic webhook-endpoint lifecycle into a
managed trigger.
Tools (6 new, 9 total):
- get_transcript, list_audit_events
- create/list/update/delete_webhook_endpoint
Triggers: note.generated, note.edited, note.access_granted, plus an all-events
trigger. The provider handler registers the Granola endpoint on deploy and
deletes it on undeploy, scoped to the trigger's own event names, and verifies
every delivery with the Standard Webhooks HMAC-SHA256 signature Granola returns
on creation. event_id is the idempotency key, which Granola reuses across
retries.
Validation fixes to the shipped tools:
- get_note dropped speaker.attribution ("me"/"them"); now surfaced
- a 413 on get_note now explains that the transcript is too large inline and
points at get_transcript, instead of surfacing a bare status code
- note IDs are URL-encoded rather than interpolated raw
- base URL, auth headers, and status-aware error handling are shared runtime
helpers; params/outputs stay literal per file so the docs generator still
reads them
Tests cover signature verification (including replay and body-tamper
rejection), event matching, subscription create/delete, and the block/tool
contract — plus a guard that ids shared between the tool and trigger surfaces
seed the same default, since block state is keyed by id and last-wins.
The knowledge-base connector was validated against the spec and needed no
changes.
…, and docs Findings from validation passes over the tools, trigger, and connector. Tools — array outputs were declared as `type: 'json'` with `properties`, which describes an object, not an array. Agents and the output picker therefore saw `notes.title` instead of `notes[i].title`. All 15 array outputs (including the pre-existing three tools) now use `type: 'array'` with `items`, matching the 2000+ other tool files. The audit event `data` field stays `json`; it is genuinely free-form per the spec. Connector — `hasMore` was ANDed with the cursor, so a `hasMore: true` response with no cursor was reported as a complete listing. The sync engine treats exactly that shape as truncated and sets `listingTruncated` to block deletion reconciliation; masking it meant a partial first page could be taken for the whole corpus and reconciliation would hard-delete every note past it. Granola would have to violate its own contract to emit that shape, but the engine already handles it and the connector was hiding the signal. Also aligns mimeType with the `.txt`/text-plain bytes the engine actually writes (it was the only connector of 101 claiming text/markdown). Trigger — the setup instructions named a Granola settings path that does not exist; the help center says Settings > Connectors > API keys in the desktop app. Both list parsers now split commas inside array entries, so an array-wrapped free-text value cannot be sent as one malformed identifier. Block — `id`, `events`, and `hasMore` are produced by several operations but their descriptions named only one, unlike `folders` which already documented both meanings. Adds connector tests pinning all four listingCapped quadrants and the truncation signal, and tool tests for the list parser and the PATCH body's per-field "omit means unchanged" semantics.
e4c912d to
4eb7645
Compare
|
@cursor review |
…tion
Raised independently by both reviewers. The registration service only rolls
external state back when createSubscription *returns* — its rollback is guarded
on `preparedProviderConfig`, so a handler that throws is assumed to have left
nothing behind. Granola's handler broke that contract: when Granola accepted the
POST but the success body was missing `id` or `signing_secret` (including a body
that failed to parse and became `{}`), it threw with the endpoint already live.
Nothing then recorded an external id, so undeploy could not remove it, and
Granola kept delivering to a callback whose signature could never be verified —
duplicating on every deploy retry.
The handler now removes what it created before rethrowing, matching the pattern
grain's multi-hook create already uses. It deletes by id when Granola returned
one, and otherwise recovers the endpoint by matching the callback URL, which
also covers a connection that fails after the request reached Granola.
Endpoints whose URL was redacted to its origin are never matched — that
comparison could delete another workflow's endpoint on the same host. Cleanup is
best effort and never masks the original failure. A non-2xx is left alone, since
no endpoint was created.
Also folds the delete call shared with deleteSubscription into one helper.
|
@cursor review |
The previous commit's URL-based recovery was unsafe. A redeploy reuses the live registration's `path`, so the candidate and the currently serving endpoint share a callback URL — listing by that URL and deleting every match would remove the live deployment's endpoint and silently stop a working trigger, which is worse than the leak it was trying to prevent. Cleanup is now keyed solely on the id Granola returned. When the success body carries no id there is no way to tell the candidate's endpoint from the live one, so it is left in place: a leaked endpoint produces unverifiable deliveries that Granola disables on its own, whereas deleting the wrong one takes down live traffic with no signal. The 2xx-missing-signing-secret case this originally fixed still cleans up, since that response does carry an id. Adds a test asserting no lookup or delete is attempted when the response has no id, so URL matching cannot be reintroduced unnoticed.
|
@cursor review |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit b46787b. Configure here.
|
On the remaining 4/5 point — the duplicate-endpoint window when execution stops after the external POST but before the id is checkpointed. That one is real, but it is not fixable inside this handler, and I don't think it should be fixed in this PR:
Blast radius if it does occur: the orphaned endpoint delivers to the same Sim path but carries the previous signing secret, so Happy to open a separate PR for the pending-create checkpoint in |
Summary
event_idis the idempotency key (Granola reuses it across retries).get_notesilently droppedspeaker.attribution("me"/"them"), so transcripts gave no way to tell the note-taker from other participants.type: 'json'withproperties, which describes an object — agents sawnotes.titleinstead ofnotes[i].title. All 15 now usetype: 'array'withitems.hasMorewas ANDed with the cursor, so ahasMore: trueresponse with no cursor was reported as a complete listing. The sync engine treats exactly that shape as truncated and blocks deletion reconciliation; masking it meant a partial page could be taken for the whole corpus and reconciliation would hard-delete the rest. Also alignsmimeTypewith the plain-text bytes the engine actually writes.params/outputsstay literal per file so the docs generator still reads them.Type of Change
Testing
Tested manually against the published OpenAPI spec (
docs.granola.ai/api-reference/openapi.json), field by field.Added tests: signature verification (replay and body-tamper rejection, multi-signature headers), event matching, subscription create/delete, the block/tool contract, all four connector
listingCappedquadrants plus the truncation signal, and the list parser / PATCH body semantics. Each was mutation-checked — reverting the fix turns the test red.bun run check:audits(30/30),bun run lint,check-block-registry,bun run type-check, and 6240 tests across tools, connectors, webhooks, triggers, and blocks all pass.Checklist