Repository navigation
Conversation
The code form of emails.send takes the fields the contract added on 2026-10-22. Addresses become lists, attachment bytes become Base64 once per send, and a Resend attachment path returns invalid_attachment before any request. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A PostHog server that doesn't take cc, bcc, headers, tags or attachments yet answers 400 invalid_request with the field in attr, so the send fails as a validation_error on that field instead of going out without it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e send A template send with one of these fields returns invalid_form on that field, instead of sending without it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Node SDK compliance v2Commit: abeb197 Capture v0 — acceptance✅ 28 passed / ❌ 3 non-passing / Drill-down: run artifacts → ❌ server_snapshot_payload_false — failed_assertionExpected native data result Code: Failed step: acceptance/public/evaluate-flags.feature:868 the public snapshot outcomes should have these semantics: ❌ server_snapshot_payload_zero — failed_assertionExpected native data result Code: Failed step: acceptance/public/evaluate-flags.feature:868 the public snapshot outcomes should have these semantics: ❌ server_snapshot_payload_null — failed_assertionExpected native data result Code: Failed step: acceptance/public/evaluate-flags.feature:868 the public snapshot outcomes should have these semantics: Harness exit: 1; report validation exit: 1. Capture v1 — acceptance✅ 28 passed / ❌ 3 non-passing / Drill-down: run artifacts → ❌ server_snapshot_payload_false — failed_assertionExpected native data result Code: Failed step: acceptance/public/evaluate-flags.feature:868 the public snapshot outcomes should have these semantics: ❌ server_snapshot_payload_zero — failed_assertionExpected native data result Code: Failed step: acceptance/public/evaluate-flags.feature:868 the public snapshot outcomes should have these semantics: ❌ server_snapshot_payload_null — failed_assertionExpected native data result Code: Failed step: acceptance/public/evaluate-flags.feature:868 the public snapshot outcomes should have these semantics: Harness exit: 1; report validation exit: 1. Capture v0 — migration✅ 55 passed / ❌ 1 non-passing / Drill-down: run artifacts → ❌ migration/yaml-parity-v1/remote-flags-v1.feature::Omitted GeoIP settings default to enabled in flag requests — failed_assertionFlags field differs Code: Failed step: migration/yaml-parity-v1/remote-flags-v1.feature:134 the first flags request field "geoip_disable" should equal JSON false Operation: Field: Expected: false Actual value: true Harness exit: 1; report validation exit: 1. Capture v1 — migration✅ 120 passed / ❌ 1 non-passing / Drill-down: run artifacts → ❌ migration/yaml-parity-v1/remote-flags-v1.feature::Omitted GeoIP settings default to enabled in flag requests — failed_assertionFlags field differs Code: Failed step: migration/yaml-parity-v1/remote-flags-v1.feature:134 the first flags request field "geoip_disable" should equal JSON false Operation: Field: Expected: false Actual value: true Harness exit: 1; report validation exit: 1. |
Coherence4 spec problems · 0 failing chokepoints · 2 spec gaps · 0 guard failures 44 components, 7 bullets (5 invariants, 2 requirements), 4 problems Spec problems (spec --check)
Chokepoints: none declared. Spec gaps: 2 entrances with outside or unknown trust and no traced control, and 10 more held by the adoption baseline
Close one with |
Moved here from PostHog#5373, closed on 2026-10-25 so this fork stack is the only copy. It opens upstream again after review.
Problem
A Resend call site that sends a copy, a custom header, a tag or a file can't move to
posthog.emails.send(): the code form takes only the content, the sender andreplyTo.This PR adds
cc,bcc,headers,tagsandattachmentsto the code form, as the shared SDK contract PostHog/posthog#120921 added them on 2026-10-22:Follow-up slice of strand Silthus/posthog#536, map Silthus/posthog#461.
Stacked on #5371 (code form), on #5312. #5372 (
emails.get) now stacks on this PR. This fork PR targetsmain, so its diff includes the PRs below it. This layer alone:feat/node-emails-code-form...feat/node-emails-code-fields.Server dependency, not merged: the fields come from PostHog/posthog#121851 (
cc,bcc,headers,tags) and PostHog/posthog#121852 (attachments), stacked on #121272. This PR is built against the contract and those serializers at the HTTP seam.Impact
Pays into Grow Workflows through billed
workflows_emailsfrom API sends, read as orgs that switched from Resend (not measured yet: the send attribution of PostHog/posthog#120628 plus its insight). In scope: Messaging, email. Expected move: small, up: it removes a blocker for Resend call sites that use these fields. Run cost: n/a.Merge Danger
Caution
🔴 Merge risk: high · one-way door (merge-risk rubric)
cc,bcc,headers,tagsandattachmentsto the public code form. Once released, the field names and attachment encoding are a contract, and a send delivers a real email, with its copies, to real recipients.Changes
Code form only. The new fields work with rendered content and with a local template. The template form's types refuse them, and at runtime a template send with one of them returns
invalid_formon that field instead of sending without it.On the wire. Each field is left out when absent.
cc,bccheaders,tagsattachments{ filename, content, content_type?, content_id? }. ABufferorUint8Arraybecomes Base64; a string goes as isOnce per send. The SDK encodes attachments when it builds the body, before the retry loop and before a local template's hash. Retries and the resend after a local-template upload send the same body. The upload never carries these fields.
An older server. A server that doesn't take one of these fields yet answers
400 invalid_requeston it; the error names it infield, so the caller sees which field to drop.A Resend
path(a URL or file path to fetch) returnsinvalid_attachment, namedvalidation_error, withfield: attachments__<n>__path, before any request. So does an attachment withoutBuffer,Uint8Arrayor string content, orattachmentsthat is not a list. The types refusepathtoo.Content stays private. The SDK never logs attachment content, and no error repeats it.
Limits stay on the server. The SDK checks types only; the server refuses more than 10 copies, reserved headers, bad tag names, blocked file types and more than 512 KB.
Parity with the contract
cc,bcc: string or listheaders: maptags:{ name, value }[]attachments:{ filename, content, content_type?, content_id? }contentBase64 from aBufferorUint8Array, a string as isinvalid_formEvidence
emailsnamespace on a real client,fetchmocked byFakeEmailsApi(emails.spec.ts).tscwith@ts-expect-errorcases (__tests__/types/emails-forms.ts).Buffer, aUint8Array, a Base64 string, and a 40 KB file that crosses the encoder's chunk size.invalid_attachmentwithout a request, for apath, for content of another type, and for anattachmentsobject that is not a list.invalid_formwith itsfieldfor each new field on the template form, without a request.400 invalid_requestnamedvalidation_error, with itsfield.ccandattachments.path.abeb1979(on the leased compute box): nodevitest(1336 passed), the edge build tests (1336 passed),tscfor the build and the type tests,oxlint,oxfmt, build andgenerate-references; corevitest.invalid_request): gap. feat(messaging): send cc, bcc, headers and tags with a code send PostHog/posthog#121851 and feat(messaging): send attachments with a code send PostHog/posthog#121852 are drafts being restacked; the end-to-end lane runs it.Release info Sub-libraries affected
Libraries affected
Checklist
If releasing new changes
pnpm changesetto generate a changeset file🤖 Agent context
Autonomy: Human-driven (agent-assisted)
Michael (@Silthus) approved adding the fields to the contract and to posthog-node. Built by Claude Code (Claude Opus 5.5) test-first, as one build lane of the map.
cc,bcc,headers,tags,attachments, code form onlycontenttypesBuffer,Uint8Arrayor a Base64 string, snake_case on the wirepathinvalid_attachmentbefore any requestbtoaover byte chunks, notBuffer, so the edge build works tooOpen: none.
Involvement: decisions by Michael through the contract; one review by GPT-6.1 Sol with no findings; a QA swarm over the restacked stack in two rounds (Review lane GPT-6.1 Sol with seven lenses, Fresh-eyes lane Claude Opus 5.5) with its fix in this PR; no live run yet.
🤖 Generated with Claude Code