feat(integrations): add iMessage via Photon - #7030
Conversation
Adds an iMessage integration backed by Photon (spectrum-ts), covering both directions: a send tool and a signed webhook trigger for inbound messages. Photon reaches iMessage over gRPC through the @spectrum-ts/imessage SDK rather than a plain REST call, so the send runs in an internal route. Spectrum instances are cached per project and evicted LRU, because constructing one mints cloud credentials and opens a connection per line. Inbound deliveries are verified with Photon's own portable verifier (@spectrum-ts/core/webhook), which implements the documented v0 scheme: HMAC-SHA256 over "v0:<timestamp>:<rawBody>" with a 5-minute replay window. Verification fails closed when no secret is configured — an inbound message can drive an agent, so an unverified delivery must never reach one. Deliveries are at-least-once, so runs are deduped on the message id. Payload mapping is taken from the SlimEnvelope schema in photon-hq/spectrum-ts rather than inferred, and a test asserts formatInput's keys match the trigger outputs exactly.
|
@qwerzl is attempting to deploy a commit to the Sim Team on Vercel. A member of the Team first needs to authorize it. |
PR SummaryHigh Risk Overview 20 operations cover send (text, media, voice, effects, threaded replies), tapbacks, edit/unsend, polls, typing/read receipts, group admin, attachment download, contact card, and chat backgrounds. Tools call internal Four triggers (message, tapback, read receipt, catch-all) share one webhook and route by content type. Verification uses Photon’s HMAC scheme and fails closed if the secret is missing; deliveries are idempotent on message id. Optional sender allowlist. Docs, icon, block/skills/templates, and tests for signatures, routing, and subscriptions are included. Reviewed by Cursor Bugbot for commit a3e3f43. Bugbot is set up for automated code reviews on this repo. Configure here. |
Greptile SummaryAdds a Photon-backed iMessage integration with twenty tool operations, four routed webhook triggers, automatic subscription management, generated catalog metadata, documentation, and tests.
Confidence Score: 4/5The shared webhook re-key lifecycle needs correction before merging because deploying or removing sibling Photon triggers can disable otherwise valid event delivery. Duplicate registration recovery replaces a shared Photon webhook while updating only one trigger record, leaving sibling records with stale verification or deletion state; the attachment-limit placement and test typing issues are additional non-blocking concerns. Files Needing Attention: apps/sim/lib/webhooks/providers/photon-imessage.ts, apps/sim/app/api/tools/photon_imessage/utils.ts, apps/sim/lib/webhooks/providers/photon-imessage.test.ts
|
| Filename | Overview |
|---|---|
| apps/sim/lib/webhooks/providers/photon-imessage.ts | Adds Photon verification, routing, formatting, deduplication, and subscription lifecycle logic; duplicate-URL re-keying can invalidate sibling trigger subscriptions. |
| apps/sim/app/api/tools/photon_imessage/utils.ts | Implements cached Spectrum clients and operation wrappers; attachment size admission occurs only after full buffering. |
| apps/sim/lib/webhooks/providers/photon-imessage.test.ts | Covers signatures, event routing, formatting, idempotency, and subscription behavior but introduces prohibited any casts. |
| apps/sim/lib/api/contracts/tools/photon-imessage.ts | Defines shared validation contracts for the newly added Photon internal tool routes. |
| apps/sim/blocks/blocks/photon_imessage.ts | Defines the Photon integration block, operation inputs, trigger configuration, and workflow-facing outputs. |
| apps/sim/triggers/photon_imessage/utils.ts | Defines shared trigger credential and sender-allowlist fields for the four Photon events. |
| apps/sim/tools/photon_imessage/utils.ts | Defines shared Photon credentials, internal request construction, target normalization, and response handling. |
Sequence Diagram
sequenceDiagram
participant User
participant Sim as Sim workflow
participant Route as Photon tool route
participant Photon
participant Webhook as Sim webhook ingress
participant Queue as Workflow queue
User->>Sim: Configure Photon trigger
Sim->>Photon: Register shared webhook URL
Photon-->>Sim: externalId + signingSecret
Sim->>Route: Execute iMessage operation
Route->>Photon: Spectrum SDK call
Photon-->>Webhook: Signed iMessage event
Webhook->>Webhook: Verify signature and route content type
Webhook->>Queue: Enqueue deduplicated workflow run
Reviews (1): Last reviewed commit: "feat(integrations): add iMessage via Pho..." | Re-trigger Greptile
| if (response.status === 409) { | ||
| // The URL is already registered — from an earlier deploy whose secret was lost with the | ||
| // trigger. The secret is only returned at creation, so re-key it: delete the stale | ||
| // registration and create a fresh one. | ||
| logger.info(`[${requestId}] Photon webhook URL already registered; re-keying`) | ||
| const listed = await photonWebhooksRequest(projectId, projectSecret, '') | ||
| const existing = (Array.isArray(listed.body.data) ? listed.body.data : []).find( | ||
| (record: PhotonWebhookRecord) => record.webhookUrl === webhookUrl | ||
| ) as PhotonWebhookRecord | undefined | ||
| if (existing?.id) { | ||
| await photonWebhooksRequest(projectId, projectSecret, `${existing.id}/`, { | ||
| method: 'DELETE', | ||
| }) | ||
| } | ||
| response = await register() |
There was a problem hiding this comment.
Shared webhook secret invalidation
When a workflow deploys multiple Photon triggers on the shared webhook URL, the later subscription deletes and recreates the existing registration but persists the new signing secret only on its own trigger record. Earlier triggers then reject valid deliveries with stale secrets, and removing the trigger that owns the replacement registration stops delivery for the remaining triggers.
Knowledge Base Used: Triggers and background automation
| const bytes = await att.read() | ||
| const buffer = Buffer.isBuffer(bytes) ? bytes : Buffer.from(bytes) | ||
| if (buffer.byteLength > MAX_DOWNLOAD_BYTES) { |
There was a problem hiding this comment.
Post-download attachment size limit
For an attachment over 50 MB, att.read() fully materializes the remote content and the code constructs a buffer before enforcing the limit. The safeguard therefore does not bound download bandwidth or memory consumption, and oversized attachments consume those resources before being rejected.
Knowledge Base Used: Integrations, connectors, and tools
| photonImessageHandler.shouldSkipEvent!({ body: textBody, requestId: 'r1' } as any) | ||
| ).toBe(false) | ||
| }) | ||
|
|
||
| it('keeps read receipts flowing so the read-receipt trigger can claim them', () => { | ||
| expect( | ||
| photonImessageHandler.shouldSkipEvent!({ body: readBody, requestId: 'r1' } as any) | ||
| ).toBe(false) | ||
| }) | ||
|
|
||
| it('skips a typing signal, which never fires any trigger', () => { | ||
| const body = { | ||
| ...textBody, | ||
| message: { ...textBody.message, content: { type: 'typing', state: 'start' } }, | ||
| } | ||
| expect(photonImessageHandler.shouldSkipEvent!({ body, requestId: 'r1' } as any)).toBe(true) | ||
| }) | ||
|
|
||
| it('skips an event that is not a message envelope', () => { | ||
| expect( | ||
| photonImessageHandler.shouldSkipEvent!({ body: { hello: 'world' }, requestId: 'r1' } as any) | ||
| ).toBe(true) |
There was a problem hiding this comment.
Webhook fixtures bypass type checking
These new shouldSkipEvent cases cast each handler context with as any, contrary to the repository's TypeScript convention. Constructing the declared context type instead keeps these tests sensitive to changes in the production handler contract.
Context Used: TypeScript conventions and type safety (source)
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
|
|
||
| logger.debug(`[${requestId}] Unknown Photon triggerId ${triggerId}, skipping`) | ||
| return false | ||
| }, |
There was a problem hiding this comment.
Outbound messages re-trigger workflows
High Severity
shouldSkipEvent and matchEvent never check message direction. Photon payloads include inbound/outbound, and outbound sends are delivered on the same webhook. A reply workflow can receive its own outbound send as a new event and loop. Peer messaging handlers such as Sendblue explicitly filter outbound deliveries.
Reviewed by Cursor Bugbot for commit 3133b23. Configure here.
| instances.delete(key) | ||
| throw error | ||
| } | ||
| } |
There was a problem hiding this comment.
Eviction can stop in-use instances
Medium Severity
evictOldest removes and stops a cached Spectrum instance whenever the pool exceeds eight projects, without waiting for in-flight tool calls on that instance to finish. Under concurrent sends across more than eight Photon projects, an active gRPC session can be torn down mid-operation and fail the request.
Reviewed by Cursor Bugbot for commit 3133b23. Configure here.
Resolves two conflicts: - scripts/check-api-validation-contracts.ts: upstream moved the route baseline to 1162; the 20 photon_imessage internal routes bring it to 1182. - apps/sim/tools/generated/tool-metadata.ts: generated artifact, regenerated from the merged tree rather than hand-merged.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.
There are 3 total unresolved issues (including 2 from previous reviews).
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit a3e3f43. Configure here.
| `Could not create the group chat. Group creation requires a dedicated Photon line. (${ | ||
| error instanceof Error ? error.message : String(error) | ||
| })` | ||
| ) |
There was a problem hiding this comment.
Group create errors always misattributed
Low Severity
The createPhotonGroup catch block wraps every failure as a dedicated-line requirement, including auth, network, and invalid-handle errors. Callers see a false root cause whenever group creation fails for any other reason.
Reviewed by Cursor Bugbot for commit a3e3f43. Configure here.


Summary
Adds an iMessage (Photon) integration: 20 operations and 4 triggers for sending and receiving iMessage through Photon, which hosts the iMessage line so no Mac has to stay online.
Sim can already reach Telegram, WhatsApp, Twilio, and Linq (iMessage/SMS/RCS). This adds a Photon-backed path with Apple-native depth that a bridge API can't reach: true unsend (retracts on the recipient's device), native polls, screen/bubble effects, chat backgrounds, tapbacks, group administration, and received attachments downloaded into the workflow as files.
Operations - send (with effects and inline replies), media, voice memos, tapbacks, polls, typing indicators, read receipts, edit, unsend, get message, download attachment, create/rename group, set group photo, add/remove participant, leave chat, get group info, share contact card, set chat background.
Triggers - message received (primary), tapback received, read receipt, and a catch-all. All four share one Photon webhook and are routed by content type; typing signals never start a run.
Setup is one click: the user enters their Photon project credentials and Sim registers the webhook on deploy, stores the returned signing secret, and deletes the registration when the trigger is removed.
Notes for reviewers
Why internal routes. Photon reaches iMessage over gRPC via the
@spectrum-ts/imessageSDK, so operations can't be plain outbound fetches from the tool layer. They run inapp/api/tools/photon_imessage/*, following the existingpostgresql/linqinternal-route pattern —checkInternalAuthbeforeparseRequest, Zod contracts inlib/api/contracts/tools/photon-imessage.ts. All 20 routes are Zod-backed (check:api-validationbaseline updated 1161 → 1181). Constructing a Spectrum instance mints cloud credentials and opens a connection per line, so instances are cached per project and evicted LRU.@spectrum-ts/imessage,@grpc/grpc-js, andnice-grpcare added toserverExternalPackages.Signature verification fails closed. Photon signs
v0:<timestamp>:<rawBody>with HMAC-SHA256. Rather than reimplement it, the handler delegates toverifySpectrumSignaturefrom@spectrum-ts/core/webhook— a runtime-agnostic Web Crypto entry — so the check stays in step with the platform. I deliberately did not usecreateHmacVerifier: it skips verification when no secret is configured, and an inbound message here can drive an agent, so an unverified delivery must never reach one. Deliveries are at-least-once, so runs dedupe on a namespaced message ID.Payload shapes aren't guessed. Per the contributing guide, the mapping comes from
slimEnvelopeSchemain photon-hq/spectrum-ts, and a test asserts each trigger'sformatInputkeys match its declaredoutputsexactly, since nothing type-checks that link.Auto-registration.
createSubscriptionPOSTs to Photon's webhook API and persists{externalId, signingSecret}viaproviderConfigUpdates(both areSYSTEM_MANAGED_FIELDS). A duplicate URL from an earlier deploy is recovered by deleting the stale registration and re-creating — the secret is only returned once, so it must be re-keyed.deleteSubscriptionis 404-tolerant and honorsstrict.Type of Change
Testing
Automated — 43 tests dedicated to this integration: signature verification (accept, tampered body, replayed timestamp, missing header, missing secret), per-trigger event routing, sender-allowlist filtering, content mapping including replies and grouped media, output-key parity per trigger, idempotency, subscription create/duplicate-recovery/401/delete-404, and block param mapping. The surrounding suites (7,200+ tests across blocks, tools, contracts, and webhooks) pass, along with
type-check,lint:check,docs:check,integration-catalog:check,deployment-config:check,tool-metadata:check,check:canvas-sentences,check:trigger-block-cycle, and the three icon checks.Live — verified against a real Photon project and a physical device: auth, both addressing paths, webhook auto-registration, an inbound message starting a run, and a reply arriving on the handset. Live testing also caught two API-design problems unit tests couldn't: the chat GUID format (
any;-;— I had guessediMessage;-;) and an over-strict "exactly one target" rule that rejected the natural wiring where recipient and chat both come from trigger outputs. The send field is now one To input accepting a phone number, Apple ID email, or chat ID, auto-detected.Focus areas for review: the fail-closed verification decision; the duplicate-registration re-key path; whether the internal-route pattern is applied correctly for a gRPC-backed provider.
Checklist
Limitations
n/a