Skip to content

fix(firestore-send-email): keep SMTP_PASSWORD available so SendGrid works with AUTH_TYPE=OAuth2 - #3089

Merged
cabljac merged 6 commits into
kitsfrom
fix/kits-send-email-sendgrid-oauth2
Sep 7, 2026
Merged

fix(firestore-send-email): keep SMTP_PASSWORD available so SendGrid works with AUTH_TYPE=OAuth2#3089
cabljac merged 6 commits into
kitsfrom
fix/kits-send-email-sendgrid-oauth2

Conversation

@cabljac

@cabljac cabljac commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

SendGrid sends fail in the kit when AUTH_TYPE=OAuth2. configFromEnv gates secrets on the auth type and forces smtpPassword to undefined under OAuth2, but transportLayer selects the SendGrid transport purely on the SMTP URI host (smtp.sendgrid.net) and passes smtpPassword as the SendGrid API key. The transport therefore got apiKey: undefined, sgMail.setApiKey was never called, and every send failed. The legacy extension reads SMTP_PASSWORD unconditionally (firestore-send-email/functions/src/config.ts and index.ts:86-90 on master), so SendGrid + OAuth2 worked there.

The fix passes the SMTP password secret through for every auth type, and secretParamsForAuthType now keeps SMTP_PASSWORD bound under OAuth2 so a future gated deploy binding cannot reintroduce the bug. New tests pin configFromEnv keeping the secret under OAuth2 and transportLayer handing the key to setApiKey; the changelog gets an entry. Rebased onto kits after #3057 merged, folding the new tests into the ported suites. Suite: 123 tests pass, tsc --noEmit clean. An adversarial review mutation-tested both halves of the fix (reverting config.ts fails the config tests, reverting the transport apiKey fails the transportLayer test) and found no regression in any consumer of smtpPassword; the obfuscated-config log path still redacts it. Caveat: no live SendGrid send was verified; the behavior is pinned at unit level against a mocked @sendgrid/mail.

Fixes #3009

@cabljac cabljac mentioned this pull request Sep 2, 2026
69 tasks

@gemini-code-assist gemini-code-assist Bot left a comment

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.

Code Review

This pull request updates the firestore-send-email kit to keep the SMTP_PASSWORD secret bound under OAuth2 authentication, ensuring that the SendGrid transport layer receives its API key. It also adds a new test suite for the transport layer and updates existing configuration tests. The reviewer noted that unconditionally binding SMTP_PASSWORD under OAuth2 would cause a usability regression for non-SendGrid OAuth2 users (e.g., Gmail OAuth2) by forcing them to provide a dummy password. To address this, the reviewer suggested conditionally binding and resolving SMTP_PASSWORD only when the SMTP connection URI points to SendGrid, moving the test cleanup hook to the top level, and adding corresponding test cases for both scenarios.

Comment thread kits/firestore-send-email/src/config.ts
Comment thread kits/firestore-send-email/src/config.ts
Comment thread kits/firestore-send-email/tests/config.test.ts
Comment thread kits/firestore-send-email/tests/config.test.ts
Comment thread kits/firestore-send-email/tests/config.test.ts
…orks with AUTH_TYPE=OAuth2

configFromEnv forced smtpPassword to undefined whenever AUTH_TYPE=OAuth2,
but transportLayer picks the SendGrid transport purely on the SMTP URI
host and passes smtpPassword as the SendGrid API key. With OAuth2 the
transport got apiKey: undefined, sgMail.setApiKey was never called, and
every send failed. The legacy extension reads SMTP_PASSWORD
unconditionally (firestore-send-email/functions/src/config.ts,
index.ts:86-90 on master), so SendGrid + OAuth2 worked there.

The config now passes the SMTP password secret through for every auth
type, and secretParamsForAuthType keeps SMTP_PASSWORD bound under OAuth2
so a future gated deploy binding cannot reintroduce the bug.

Fixes #3009
@cabljac
cabljac force-pushed the fix/kits-send-email-sendgrid-oauth2 branch from 5acfcdc to 85f5cc7 Compare September 2, 2026 12:51
@cabljac
cabljac marked this pull request as ready for review September 2, 2026 12:55

@IzaakGough IzaakGough left a comment

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.

The fix looks right. transportLayer checks isSendGrid before setSmtpCredentials, so the SendGrid branch is reachable under AUTH_TYPE=OAuth2 and now gets a real API key, and un-gating smtpPassword is safe for the OAuth2 non-SendGrid path because setSmtpCredentials returns setupOAuth2 before touching it. logs.ts still redacts it, and nothing else reads it. Verified locally at this head: 123 tests pass, typecheck and prettier clean, and restoring the old OAuth2 gate makes the new config test fail, so it does pin the behaviour.

One thing outside the diff that I think is worth including here: kits/firestore-send-email/README.md around line 156 still tells OAuth2 installs to create SMTP_PASSWORD with a placeholder value. Someone on SendGrid who follows that ends up at sgMail.setApiKey("<placeholder>") and every send fails with a 401, so the setup path the CHANGELOG entry describes as fixed still breaks. A short carve-out saying the real API key goes in SMTP_PASSWORD when the connection URI is smtp.sendgrid.net, whatever AUTH_TYPE is set to, would cover it.

The two inline notes are both about test durability rather than current behaviour, so neither needs to block.

Comment thread kits/firestore-send-email/tests/config.test.ts Outdated
Comment thread kits/firestore-send-email/tests/helpers.test.ts
Comment thread kits/firestore-send-email/src/config.ts
@sendgrid/mail exports a MailService instance, so setApiKey and send are
prototype methods. Under esModuleInterop the namespace import compiles to
__importStar, which copies own properties only, so both were dropped and
every SendGrid send threw, at init once an API key was present and at send
time otherwise. The legacy extension has the same import line but no
esModuleInterop, so it was unaffected.

The mocks now expose the module as both the default and the named exports,
matching its real shape. The new test runs against lib/ because vitest's
transform does not reproduce the tsc emit that causes this.
@IzaakGough

Copy link
Copy Markdown
Contributor

Closed out the testing on a live SendGrid key, running the kit in the functions emulator with AUTH_TYPE=OAuth2 and the SendGrid connection URI.

The config change does what it says. smtpPassword now reaches the transport, where before it was absent from the resolved config entirely. With a real key the request authenticates and SendGrid answers at the account level, so setApiKey and send both complete the round trip. That last part only became reachable with 2c2a325, since before it the transport threw on setApiKey regardless of auth type.

What is not proven: the send stopped at Maximum credits exceeded, so delivery.state: SUCCESS and the sendgridQueueId parsing never ran. That path is untouched here and covered by the existing unit tests, so it did not seem worth chasing a credited account. The control is useful though, a deliberately fake key on the same rig returns The provided authorization grant is invalid, expired, or revoked, so the credits response is SendGrid resolving the key to an account rather than rejecting it.

All of this was the emulator rather than a real deploy, so deploy-time secret binding onto the service is still untested.

…SMTP_PASSWORD

The OAuth2 setup step told every install to create SMTP_PASSWORD with a
placeholder, which leaves SendGrid users failing every send with a 401.
…tLayer tests

toHaveBeenCalledWith matches any recorded call, so without the reset the
setApiKey assertion could be satisfied by an earlier test's call.
@IzaakGough

IzaakGough commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

Took this to a live deploy on a test project to cover the one thing the emulator cannot reach, deploy-time secret binding.

The deployed service binds all four secrets as env vars, SMTP_PASSWORD included, under AUTH_TYPE=OAuth2. A document written to the real mail collection was picked up, the function read the key out of Secret Manager, built the SendGrid transport and authenticated, landing on the same Maximum credits exceeded as the emulator run. The transport constructing at all is 2c2a325 working in a deployed build, since the previous one died at setApiKey before any network call.

One thing found along the way, separate from this PR's subject. Secret params resolve to two different resources depending on which layer of the deploy is asking. applyPrefix rewrites each endpoint's secret env vars to kit-<instance id>-<PARAM>, but it does not set resourceId on the params, so the params layer falls through to the bare name. Both halves are observable:

  • The params layer reaches for the bare name. An instance with no secrets of its own bound onto an unrelated project level SMTP_PASSWORD that happened to exist, and a missing one reports In non-interactive mode but have no value for the secret CLIENT_ID: CLIENT_ID, followed by Set this secret before deploying: firebase functions:secrets:set CLIENT_ID.
  • Endpoint validation then asks for the prefixed name: Secret [projects/.../secrets/kit-default-SMTP_PASSWORD] not found or has no versions.

So following the CLI's own suggestion creates a secret the deploy then ignores, and functions:secrets:set cannot create the prefixed name either, since it uppercases the argument and KIT_ is a reserved prefix. Creation uses that same bare resourceId, so the interactive prompt should land in the same place, though I did not drive the prompt itself.

What does work today is FIREBASE_SECRET_REF_<PARAM>=projects/<p>/secrets/<name>/versions/latest in the instance's .env. It overrides both layers, takes any resource name, and needs no prompt, so it is also the only CI safe route. That one I confirmed with a dry run rather than a completed deploy. Creating kit-<instance>-<PARAM> by hand with gcloud works too, and is what got the deploy above through.

For anyone coming from an extension none of this should be manual. firebase ext:export --mode functions writes those same FIREBASE_SECRET_REF_ entries pointing at the live ext-<instance>-<PARAM> resources, with a secret ejection prompt alongside. That sits behind the private extMigrationFeatures experiment and ext:migrate is currently a stub, so it is not usable yet. That paragraph is from reading firebase-tools 15.29.0 rather than running it.

This looks like a firebase-tools issue rather than anything to change in this PR, and the kit README wording is downstream of whatever it settles on.

…m mock

AUTH_TYPE, USER and HOST are all param names that exist as ambient shell
vars, and AUTH_TYPE is a real key in the kit's own .env, so sourcing it
failed the suite. The mock now reads a map the tests control.
@cabljac
cabljac merged commit 070460e into kits Sep 7, 2026
11 checks passed
cabljac added a commit that referenced this pull request Sep 8, 2026
…for function placement (#3102)

Fixes #3069. The kit passed the raw DATABASE_REGION value as the
function's region, and the param's select offers the Firestore
multi-regions eur3/nam5/nam7, which are not Cloud Run regions, so
multi-region deploys hard-failed.

The issue proposed dropping the region option (the #3066 pattern). This
PR deliberately deviates: by agreement with the firebase-tools team,
DATABASE_REGION stays and is mapped instead, because the CLI's own
inference is unreliable for param-declared databases
(firebase/firebase-tools#11020) and the extension-to-kit migration
exports DATABASE_REGION into the user's .env. Multi-regions map to a
region inside them (nam5/nam7 to us-central1, eur3 to europe-west1,
mirroring the CLI's FIRESTORE_DUAL_REGION_TO_REGION_MAPPING); regional
locations pass through; unset or empty means no region option and the
CLI fallback. #3101 is the sibling fix for firestore-bigquery-export.

Tests pin all mapping cases plus the unset case; breaking the mapping
fails them. Caveats: no live multi-region deploy was run for this kit,
and the module-load process.env read needs firebase-tools >= 15.28.0
during discovery (older CLIs degrade to the no-region fallback). The
approved #3089 also touches src/config.ts; this change edits only
envDeployOptions at the end of the file, so overlap is limited to a
trivial CHANGELOG conflict.
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.

fix(firestore-send-email): SendGrid sends fail with AUTH_TYPE=OAuth2

2 participants