feat: photon imessage integration - #7042
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.
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.
Restores type-check: `downloadServableFileFromStorage` made `maxBytes` required upstream, and the merge at a3e3f43 left the Photon media helper calling it with three arguments. Passing the 100MB ceiling also bounds the upload read — the declared size is checked before any bytes move — closing the upload-side twin of the download issue reviewers flagged. Outbound deliveries no longer reach a trigger. Photon echoes our own sends back on the same webhook tagged `outbound`, so a reply workflow could be re-triggered by its own reply and loop. `shouldSkipEvent` drops them before routing, testing for an explicit `outbound` rather than for `inbound` so a delivery that omits the optional field keeps firing as it does today. Eviction can no longer close a connection mid-operation. The hand-rolled Map LRU becomes an `LRUCache` (per .claude/rules/sim-caching.md) whose entries carry a lease count: eviction only marks an entry, and `stop()` runs when the last in-flight operation releases it. All 20 operations run through `withPhotonContext`, which holds the lease in a `finally`. The entry is published before the first await, so concurrent callers for one project share a single construction instead of racing two connections. Also: `getPhotonMessage` surfaces native voice memos as attachments (with `size`), matching the webhook's `collectAttachments`, so Download Attachment can be driven from its output; oversized attachments are rejected on their declared size before any bytes are fetched, with the post-read check kept as a backstop; only an `UnsupportedError` maps to the dedicated-line message, so auth, network, and invalid-handle failures keep their own cause; the webhook fixtures drop 11 `as any` casts for the real handler context types; and the local `isRecord` gives way to `isRecordLike` from `@sim/utils/object`. Adds 11 tests: pooling reuse, the lease-vs-eviction race, failed-construction eviction, voice attachments through reply/group wrappers, both size-limit paths, both group-create error paths, and the direction filter.
|
@qwerzl is attempting to deploy a commit to the Sim Team on Vercel. A member of the Team first needs to authorize it. |
PR SummaryMedium Risk Overview Product surface: A new Runtime: Operations run through internal Inbound: A webhook provider verifies Photon signatures fail-closed, skips outbound/typing noise, routes events by trigger type and optional sender allowlist, dedupes on message ID, and auto-registers webhooks on deploy (including 409 re-key) while cleaning up on trigger removal. Reviewed by Cursor Bugbot for commit 3b7923e. Bugbot is set up for automated code reviews on this repo. Configure here. |
Greptile SummaryAdds a Photon-backed iMessage integration with twenty workflow operations, four signed webhook triggers, automatic subscription management, SDK connection pooling, documentation, and catalog registration.
Confidence Score: 4/5The PR should not merge until duplicate-registration recovery handles failed stale-webhook deletion instead of immediately retrying against the same registration. A failed DELETE in the 409 recovery path is silently discarded, causing the subsequent registration attempt to fail while Sim still lacks the one-time signing secret needed for the existing webhook; the remaining documentation-comment issue is non-blocking. Files Needing Attention: apps/sim/lib/webhooks/providers/photon-imessage.ts, apps/sim/app/api/tools/photon_imessage/utils.ts
|
| Filename | Overview |
|---|---|
| apps/sim/lib/webhooks/providers/photon-imessage.ts | Implements signed delivery handling and subscription lifecycle; duplicate recovery retries registration without validating stale-webhook deletion. |
| apps/sim/app/api/tools/photon_imessage/utils.ts | Implements pooled Spectrum clients and the Photon operation mappings, with one declaration comment that violates the repository's TSDoc convention. |
| apps/sim/lib/api/contracts/tools/photon-imessage.ts | Defines the shared request and response contracts for the twenty internal Photon operation routes. |
| apps/sim/blocks/blocks/photon_imessage.ts | Adds the workflow block configuration for Photon operations and triggers. |
| apps/sim/triggers/photon_imessage/webhook.ts | Defines the catch-all Photon trigger and its declared workflow output shape. |
| apps/sim/next.config.ts | Externalizes the native gRPC-related Photon dependencies for the Next.js server runtime. |
| packages/deployment-config/src/integrations.json | Registers Photon and its operations and triggers in deployment-facing integration metadata. |
Sequence Diagram
sequenceDiagram
participant Photon
participant Webhook as Sim webhook endpoint
participant Provider as Photon provider
participant Queue as Workflow queue
participant Workflow
Photon->>Webhook: Signed iMessage delivery
Webhook->>Provider: Verify signature and timestamp
Provider->>Provider: Filter direction, type, and sender
Provider->>Provider: Extract namespaced message ID
Provider->>Queue: Enqueue matching trigger input
Queue->>Workflow: Run deployed workflow
Workflow-->>Photon: Optional Photon tool operation
Reviews (1): Last reviewed commit: "fix(integrations): address Photon iMessa..." | Re-trigger Greptile
The 409 re-key branch no longer discards the outcome of the steps it depends on. Re-registering over a stale record that is still there just earns another 409, so a failed listing or a failed DELETE now surfaces as the thing an operator can act on — delete the webhook in the Photon dashboard and redeploy — instead of a second conflict with no explanation. One content walker, not two. `extractText` claimed to mirror the webhook's `collectText` but returned only a group's first text node and counted an empty string as a hit, so Get Message and the trigger could report different `text` for the same grouped delivery. Both surfaces now read through `@/lib/photon-imessage/content`. Sharing it also settles the attachment shape: absent fields are `null` on both sides rather than `null` on one and `''` on the other. This is the third bug from these two copies drifting, so the copies are gone rather than re-aligned. Media file inputs are required for `send_media` and `send_voice_memo`, matching Linq's attachment pattern, so the editor catches a missing file instead of letting the run fail on a route 400. The clearable ops keep an optional file — `imageAction: 'clear'` is a valid way to run those without one. Both members of the canonical pair carry the same condition, as the group requires. Also converts the `messageEffects` declaration comment to TSDoc. Adds 24 tests: the walkers' group/reply/voice handling and the parity the shared module exists to guarantee, plus both new 409-recovery failure paths.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 4e11e89. Configure here.
… stuck registration The 409 re-key path read any missing match as "registered but Photon did not return it" and told the operator to delete the webhook by hand — including when the list call itself failed, where the right answer is simply to deploy again. A failed listing now throws its own retry-shaped error before the match is looked for, so manual cleanup is only ever advised for the state that actually requires it, and nothing is deleted off a read that never succeeded.

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