Skip to content

feat(integrations): add iMessage via Photon - #7030

Closed
qwerzl wants to merge 2 commits into
simstudioai:mainfrom
photon-hq:feat/photon-imessage-integration
Closed

feat(integrations): add iMessage via Photon#7030
qwerzl wants to merge 2 commits into
simstudioai:mainfrom
photon-hq:feat/photon-imessage-integration

Conversation

@qwerzl

@qwerzl qwerzl commented Aug 24, 2026

Copy link
Copy Markdown

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/imessage SDK, so operations can't be plain outbound fetches from the tool layer. They run in app/api/tools/photon_imessage/*, following the existing postgresql/linq internal-route pattern — checkInternalAuth before parseRequest, Zod contracts in lib/api/contracts/tools/photon-imessage.ts. All 20 routes are Zod-backed (check:api-validation baseline 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, and nice-grpc are added to serverExternalPackages.

Signature verification fails closed. Photon signs v0:<timestamp>:<rawBody> with HMAC-SHA256. Rather than reimplement it, the handler delegates to verifySpectrumSignature from @spectrum-ts/core/webhook — a runtime-agnostic Web Crypto entry — so the check stays in step with the platform. I deliberately did not use createHmacVerifier: 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 slimEnvelopeSchema in photon-hq/spectrum-ts, and a test asserts each trigger's formatInput keys match its declared outputs exactly, since nothing type-checks that link.

Auto-registration. createSubscription POSTs to Photon's webhook API and persists {externalId, signingSecret} via providerConfigUpdates (both are SYSTEM_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. deleteSubscription is 404-tolerant and honors strict.

Type of Change

  • Bug fix
  • New feature
  • Breaking change
  • Documentation
  • Other: ___________

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 guessed iMessage;-;) 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

  • Code follows project style guidelines
  • Self-reviewed my changes
  • Tests added/updated and passing
  • No new warnings introduced
  • I confirm that I have read and agree to the terms outlined in the Contributor License Agreement (CLA)

Limitations

n/a

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
qwerzl requested a review from a team as a code owner August 24, 2026 04:42
@vercel

vercel Bot commented Aug 24, 2026

Copy link
Copy Markdown

@qwerzl is attempting to deploy a commit to the Sim Team on Vercel.

A member of the Team first needs to authorize it.

@cursor

cursor Bot commented Aug 24, 2026

Copy link
Copy Markdown

PR Summary

High Risk
New inbound webhook path that can start agent workflows, plus project secrets, HMAC verification, gRPC client caching, and file ACL-gated attachment handling.

Overview
Adds iMessage (Photon) so workflows can send and receive native iMessage through a Photon-hosted line (no Mac). Users enter project credentials; Sim registers the webhook on deploy, stores the signing secret, and cleans it up on trigger removal.

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 /api/tools/photon_imessage/* routes because Photon is gRPC via @spectrum-ts/imessage (kept external in Next). Spectrum instances are LRU-cached per project.

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-apps

greptile-apps Bot commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

Adds a Photon-backed iMessage integration with twenty tool operations, four routed webhook triggers, automatic subscription management, generated catalog metadata, documentation, and tests.

  • Adds authenticated internal routes and Spectrum SDK wrappers for messaging, attachment, group, and presence operations.
  • Adds signed webhook verification, trigger-specific event mapping, sender filtering, idempotency, and subscription lifecycle handling.
  • Registers the integration across blocks, tools, triggers, deployment configuration, icons, and generated documentation.

Confidence Score: 4/5

The 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

Important Files Changed

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
Loading

Reviews (1): Last reviewed commit: "feat(integrations): add iMessage via Pho..." | Re-trigger Greptile

Comment on lines +369 to +383
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()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 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

Comment on lines +528 to +530
const bytes = await att.read()
const buffer = Buffer.isBuffer(bytes) ? bytes : Buffer.from(bytes)
if (buffer.byteLength > MAX_DOWNLOAD_BYTES) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 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

Comment on lines +155 to +176
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)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 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
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 3133b23. Configure here.

Comment thread apps/sim/app/api/tools/photon_imessage/utils.ts
instances.delete(key)
throw error
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.

Fix in Cursor Fix in Web

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.

@cursor cursor 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.

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).

Fix All in Cursor

❌ 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)
})`
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit a3e3f43. Configure here.

@qwerzl qwerzl closed this Aug 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant