Skip to content

Add marketing tool API endpoints#78

Open
peachbits wants to merge 4 commits into
masterfrom
matthew/marketing-push-tool
Open

Add marketing tool API endpoints#78
peachbits wants to merge 4 commits into
masterfrom
matthew/marketing-push-tool

Conversation

@peachbits

@peachbits peachbits commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

CHANGELOG

Does this branch warrant an entry to the CHANGELOG?

  • Yes
  • No

Dependencies

none

Description

Adds a self-serve marketing push API to edge-push-server so the marketing team can send notifications without developer help. The web UI is hosted separately in the internal-tools project, which proxies these endpoints.

Two endpoints under /marketing, mirroring the send-message and push-marketing CLI commands:

  • POST /marketing/api/test-message — a single test push to a device or login.
  • POST /marketing/api/push-marketing — an audience blast, streaming its progress log back to the caller.

Key points:

  • Auth: every request must carry a valid x-api-key, looked up in the API-key database via getApiKeyByKey (same source as the v2 routes). No shared secret.
  • Scoping: the blast targets only the devices registered under that API key, then by location, via a new apiKeyLocation CouchDB view.
  • Open tracking: each blast generates a campaignId echoed in the notification data (with an optional deep-link url), so the app can report opens to analytics. Test pushes carry a fixed test campaign id so they exercise the same navigation + tracking path while staying distinguishable from real campaigns.
  • Adds pushServerConfig.sample.json documenting the config shape (the real config is gitignored).

Note

Medium Risk
New HTTP surface can trigger real marketing blasts to many devices; mitigated by API-key lookup and per-key device scoping, but misconfiguration or key compromise could still cause broad sends.

Overview
Adds a /marketing Express router (mounted before Serverlet) so internal tools can trigger pushes without the CLI.

POST /marketing/api/test-message sends one test notification to a deviceId or loginId, with optional title/body/url and a fixed campaignId of test for analytics. POST /marketing/api/push-marketing mirrors the marketing blast flow: requires confirmed, optional geo filters, streams plain-text progress (device count, campaign UUID, heartbeat, per-device errors), and sends marketing payloads with a generated campaignId and optional deep link.

Both routes authenticate via x-api-key (getApiKeyByKey). Audience targeting is scoped to devices for that API key using a new Couch apiKeyLocation view plus countDevicesByApiKeyLocation / streamDevicesByApiKeyLocation (prefix keys by country/region/city). Also adds pushServerConfig.sample.json and a CHANGELOG unreleased note.

Reviewed by Cursor Bugbot for commit e87a23a. Bugbot is set up for automated code reviews on this repo. Configure here.

Document the server config shape in a committed sample file, since the
real pushServerConfig.json is gitignored. Copy it to pushServerConfig.json
and fill in real values to run the server.
Expose two endpoints under /marketing that mirror the send-message and
push-marketing CLI commands, so the marketing team can send a test push
and then a marketing blast without developer help.

Authentication uses the API keys in the push database (getApiKeyByKey),
matching the v2 routes: every request must carry a valid x-api-key
header. The push-marketing send is scoped to the devices registered
under that key, then by location, via a new apiKeyLocation view. Each
blast gets a generated campaignId echoed in the notification data (with
an optional deep-link url) so the app can report opens to analytics.
Give a test-message push a fixed 'test' campaign id and an optional
deep-link url, so tapping a test opens the same marketing path in the app
(navigation plus open tracking) while its analytics stay distinguishable
from real campaigns.
}

if (loginId != null) {
await sender.sendToLogin(loginId, message)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Agentic Security Review
Severity: HIGH
/marketing/api/test-message authenticates that an API key exists but does not verify that the supplied loginId (or alternate deviceId path) belongs to that authenticated key before sending. This allows a caller with any valid key to target identifiers outside its own API-key scope.

Impact: Cross-tenant unauthorized push delivery is possible if attacker-controlled requests include known or guessable target identifiers.

Fix in Cursor Fix in Web

Reviewed by Cursor Security Reviewer for commit e87a23a. Configure here.

@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 default effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Want higher recall? High effort reviews run extra passes and find more bugs. A team admin can switch effort levels in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit e87a23a. Configure here.

Comment thread src/db/couchDevices.ts
key.push(part)
}
return key
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

City ignored without region

Medium Severity

When a blast request includes country and city but omits region, makeApiKeyLocationStartKey stops after country, so CouchDB returns every device under that API key in the country instead of the requested city. The UI can still print the city in the log line.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit e87a23a. Configure here.

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