Skip to content
This repository was archived by the owner on Aug 3, 2026. It is now read-only.

Commit 71200ef

Browse files
authored
Merge pull request #4 from ESPToolKit/feature/esp-webpush-v2
Feature/esp webpush v2
2 parents c9945c3 + d91f2d7 commit 71200ef

15 files changed

Lines changed: 1369 additions & 596 deletions

File tree

.github/workflows/ci.yml

Lines changed: 52 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ name: CI
22

33
on:
44
push:
5-
branches: [ main, master ]
5+
branches: [ main, master, 'feature/**' ]
66
tags: ['v*']
77
pull_request:
88
workflow_dispatch:
@@ -49,13 +49,62 @@ jobs:
4949
--project-option "platform=https://github.com/pioarduino/platform-espressif32.git" \
5050
--project-option "build_unflags=-std=gnu++11" \
5151
--project-option "build_flags=-std=gnu++17" \
52-
--project-option "lib_deps=ArduinoJson@>=7.0.0, https://github.com/ESPToolKit/esp-worker.git"
52+
--project-option "lib_deps=ArduinoJson@>=7.0.0"
5353
fi
5454
done
5555
56-
arduino-cli:
56+
espidf-smoke:
5757
runs-on: ubuntu-latest
5858
needs: build-examples
59+
steps:
60+
- name: Checkout
61+
uses: actions/checkout@v4
62+
63+
- name: Set up Python
64+
uses: actions/setup-python@v5
65+
with:
66+
python-version: '3.13'
67+
68+
- name: Cache PlatformIO
69+
uses: actions/cache@v4
70+
with:
71+
path: ~/.platformio
72+
key: ${{ runner.os }}-platformio-${{ hashFiles('**/library.json') }}
73+
restore-keys: |
74+
${{ runner.os }}-platformio-
75+
76+
- name: Install PIOArduino Core
77+
run: python -m pip install --upgrade https://github.com/pioarduino/platformio-core/archive/refs/tags/v6.1.18.zip
78+
79+
- name: Install PIOArduino ESP32 Platform
80+
run: pio platform install https://github.com/pioarduino/platform-espressif32.git
81+
82+
- name: Build ESP-IDF smoke target
83+
run: |
84+
set -euo pipefail
85+
tmpdir="$(mktemp -d)"
86+
cat > "${tmpdir}/main.cpp" <<'EOF'
87+
#include <ESPWebPush.h>
88+
89+
extern "C" void app_main(void) {
90+
ESPWebPush webPush;
91+
WebPushVapidConfig vapid{};
92+
vapid.subject = "mailto:test@example.com";
93+
}
94+
EOF
95+
96+
pio ci "${tmpdir}/main.cpp" \
97+
--board esp32dev \
98+
--lib="." \
99+
--project-option "platform=https://github.com/pioarduino/platform-espressif32.git" \
100+
--project-option "framework=espidf" \
101+
--project-option "build_unflags=-std=gnu++11" \
102+
--project-option "build_flags=-std=gnu++17" \
103+
--project-option "lib_deps=ArduinoJson@>=7.0.0"
104+
105+
arduino-cli:
106+
runs-on: ubuntu-latest
107+
needs: [build-examples, espidf-smoke]
59108
env:
60109
ESP32_CORE_VERSION: 3.3.3
61110
ESP32_PACKAGE_URL: https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json
@@ -104,7 +153,6 @@ jobs:
104153
run: |
105154
arduino-cli lib update-index
106155
arduino-cli lib install "ArduinoJson"
107-
arduino-cli lib install --git-url "https://github.com/ESPToolKit/esp-worker.git"
108156
109157
- name: Add local library to sketchbook
110158
run: |

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1-
.venv
1+
.pio/
2+
.venv

.pio/build/project.checksum

Lines changed: 0 additions & 1 deletion
This file was deleted.

CHANGELOG.md

Lines changed: 27 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,23 +4,35 @@ All notable changes to this project will be documented in this file.
44

55
## [Unreleased]
66

7+
### Changed
8+
- Breaking: renamed the public transport struct from `Subscription` to `WebPushSubscription` everywhere with no compatibility alias.
9+
- Breaking: renamed `PushMessage.sub` to `PushMessage.subscription` for API consistency.
10+
- Breaking: removed app-level metadata fields `deviceId`, `disabledTags`, and `deleted` from the transport struct.
11+
- `validateSubscription()` now validates only the required Web Push transport fields: `endpoint`, `p256dh`, and `auth`.
12+
13+
## [2.0.0] - 2026-03-28
14+
715
### Added
8-
- Core ESPWebPush implementation: VAPID JWT signing, AES-GCM payload encryption, and HTTP delivery.
9-
- Async queue + worker task with configurable stack, priority, queue length, and memory caps.
10-
- Sync `send()` API returning structured `WebPushResult`.
11-
- Retry/backoff handling for network/transport failures.
12-
- Basic example sketch and CI workflows.
13-
- Teardown lifecycle tests for pre-init `deinit()`, idempotent `deinit()`, re-init, and destructor teardown.
14-
- Strict `PushPayload` API with typed notification fields and ArduinoJson v7+ overloads.
15-
- User-provided network validator callback support.
16+
- `WebPushVapidConfig` with standards-based `subject`, public key, and private key inputs.
17+
- `WebPushEnqueueResult` for async preflight / queue outcomes.
18+
- `WebPushJoinStatus` plus `requestStop()` / `join(timeoutMs)` for bounded worker shutdown.
19+
- RFC 8291 Appendix A key-derivation and encrypted-body test coverage.
20+
- Payload-size guard with the RFC-safe default limit of 3993 bytes.
21+
- Small per-origin JWT cache for VAPID header reuse.
1622

1723
### Changed
18-
- Teardown contract now uses `isInitialized()` and removes the old `initialized()` naming.
19-
- `deinit()` now always converges teardown, including worker/queue/crypto cleanup and runtime config/key release.
20-
- Structured payload inputs now reject unknown fields, missing required fields, and invalid types before enqueue/send.
21-
- ArduinoJson v7+ is now an explicit dependency.
24+
- Reworked encryption and transport to use RFC 8188 / RFC 8291 `aes128gcm` only.
25+
- `init()` now validates `mailto:` / `https://` VAPID subjects and verifies that the configured public key matches the private key.
26+
- Async `send()` overloads now return `WebPushEnqueueResult` and only invoke callbacks for queued work.
27+
- `deinit()` now returns `WebPushJoinStatus` and uses a bounded stop/join flow instead of waiting forever.
28+
- JWT payload assembly now uses dynamic `std::string` construction instead of a fixed stack buffer.
29+
- Structured and raw payload sends now enforce the payload-size guard before transport.
30+
- README, example sketch, package metadata, and CI now describe the v2 API and drop stale `esp-worker` references.
31+
- CI push triggers now include `feature/**` branches so v2 work runs workflows before merge.
32+
- `library.json` now advertises both Arduino and ESP-IDF compatibility.
33+
- Package metadata now reports the breaking release as `2.0.0`.
2234

2335
### Notes
24-
- JWT signing requires a valid system clock (SNTP).
25-
- Content encoding uses `aesgcm` with VAPID headers (`Authorization`, `Crypto-Key`, `Encryption`).
26-
- Worker configuration now uses `WebPushWorkerConfig` with native FreeRTOS task creation.
36+
- JWT signing still requires a valid system clock (SNTP).
37+
- Push sends use `Content-Encoding: aes128gcm` with VAPID `Authorization`.
38+
- Breaking changes in v2 include the new `init()` signature, async enqueue return type, bounded shutdown API, and RFC 8291-only protocol behavior.

README.md

Lines changed: 72 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,24 @@
11
# ESPWebPush
22

3-
ESPWebPush is an **async-first** Web Push sender for ESP32 firmware. It handles VAPID JWT signing, Web Push AES-GCM payload encryption, and HTTP delivery so your devices can notify browsers without extra glue code.
3+
ESPWebPush is an async-first Web Push sender for ESP32 firmware. It handles VAPID JWT signing, RFC 8291 `aes128gcm` payload encryption, and HTTP delivery so devices can notify browsers without custom glue code.
44

5-
ArduinoJson v7+ is a required dependency for the structured payload API.
5+
ArduinoJson v7+ is required for the structured payload API.
66

77
## CI / Release / License
88
[![CI](https://github.com/ESPToolKit/esp-webPush/actions/workflows/ci.yml/badge.svg)](https://github.com/ESPToolKit/esp-webPush/actions/workflows/ci.yml)
99
[![Release](https://img.shields.io/github/v/release/ESPToolKit/esp-webPush?sort=semver)](https://github.com/ESPToolKit/esp-webPush/releases)
10-
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE.md)
10+
[![License: MIT](https://img.shields.io/github/license/ESPToolKit/esp-webPush)](LICENSE.md)
1111

1212
## Features
13-
- VAPID JWT signing (ES256) from base64url private key.
14-
- Web Push AES-GCM payload encryption.
13+
- RFC 8292 VAPID JWT signing with `mailto:` or `https://` subjects.
14+
- RFC 8291 / RFC 8188 `aes128gcm` Web Push encryption.
1515
- Async queue + worker task via native FreeRTOS APIs.
16-
- Optional synchronous `send()` API.
16+
- Bounded shutdown via `requestStop()`, `join(timeoutMs)`, and `deinit(timeoutMs)`.
17+
- Sync `send()` API plus async `send()` overloads that return `WebPushEnqueueResult`.
1718
- Strict `PushPayload` validation for browser notification fields.
18-
- ArduinoJson v7+ overloads for validated `JsonDocument` / `JsonVariantConst` payloads.
19-
- Configurable queue length, memory caps (internal vs PSRAM), stack, priority, retries, and timeouts.
20-
- Optional application-provided network validator callback.
21-
- Uses the standard Web Push headers (`Authorization`, `Crypto-Key`, `Encryption`, `TTL`).
19+
- Payload-size guard with the RFC-safe default limit of 3993 bytes.
20+
- Small per-origin JWT cache to avoid re-signing every message.
21+
- Configurable queue length, memory caps, retries, timeouts, and worker task settings.
2222

2323
## Quick Start
2424

@@ -31,33 +31,35 @@ ESPWebPush webPush;
3131
void setup() {
3232
Serial.begin(115200);
3333

34+
WebPushVapidConfig vapid;
35+
vapid.subject = "mailto:notify@example.com";
36+
vapid.publicKeyBase64 = "BAvapidPublicKeyBase64Url...";
37+
vapid.privateKeyBase64 = "vapidPrivateKeyBase64Url...";
38+
3439
WebPushConfig cfg;
3540
cfg.queueLength = 16;
3641
cfg.queueMemory = WebPushQueueMemory::Psram;
3742
cfg.worker.stackSizeBytes = 16 * 1024;
3843
cfg.worker.priority = 3;
3944
cfg.worker.name = "webpush";
45+
cfg.maxPayloadBytes = 3993;
4046
cfg.networkValidator = []() { return true; };
4147

42-
webPush.init(
43-
"notify@example.com",
44-
"BAvapidPublicKeyBase64Url...",
45-
"vapidPrivateKeyBase64Url...",
46-
cfg);
48+
webPush.init(vapid, cfg);
4749
}
4850

4951
void loop() {}
5052
```
5153

5254
## Usage
5355

54-
### Subscription / Structured Payload
56+
### WebPushSubscription / Structured Payload
5557

5658
```cpp
57-
Subscription sub;
58-
sub.endpoint = "https://fcm.googleapis.com/fcm/send/...";
59-
sub.p256dh = "BME..."; // base64url from browser subscription
60-
sub.auth = "nsa..."; // base64url from browser subscription
59+
WebPushSubscription subscription;
60+
subscription.endpoint = "https://fcm.googleapis.com/fcm/send/...";
61+
subscription.p256dh = "BME...";
62+
subscription.auth = "nsa...";
6163

6264
PushPayload payload;
6365
payload.title = "Hello";
@@ -69,7 +71,7 @@ payload.icon = "https://example.com/icon.png";
6971
### Async Send
7072

7173
```cpp
72-
bool started = webPush.send(sub, payload, [](WebPushResult result) {
74+
WebPushEnqueueResult enqueue = webPush.send(subscription, payload, [](WebPushResult result) {
7375
if (!result.ok()) {
7476
ESP_LOGE("WEBPUSH", "Push failed: %s (status %d)",
7577
result.message, result.statusCode);
@@ -78,11 +80,13 @@ bool started = webPush.send(sub, payload, [](WebPushResult result) {
7880
ESP_LOGI("WEBPUSH", "Push OK (status %d)", result.statusCode);
7981
});
8082

81-
if (!started) {
82-
ESP_LOGW("WEBPUSH", "Queue full or not initialized");
83+
if (!enqueue.queued()) {
84+
ESP_LOGW("WEBPUSH", "Enqueue failed: %s", enqueue.message);
8385
}
8486
```
8587

88+
Async preflight failures are returned through `WebPushEnqueueResult`. The callback only runs for messages that were actually queued.
89+
8690
### ArduinoJson v7+ Send
8791

8892
```cpp
@@ -91,13 +95,13 @@ doc["title"] = "Hello";
9195
doc["body"] = "ESP32";
9296
doc["tag"] = "demo";
9397

94-
WebPushResult result = webPush.send(sub, doc);
98+
WebPushResult result = webPush.send(subscription, doc);
9599
```
96100

97101
### Sync Send
98102

99103
```cpp
100-
WebPushResult result = webPush.send(sub, payload);
104+
WebPushResult result = webPush.send(subscription, payload);
101105
if (!result.ok()) {
102106
ESP_LOGW("WEBPUSH", "Sync push failed: %s", result.message);
103107
}
@@ -107,7 +111,7 @@ if (!result.ok()) {
107111

108112
```cpp
109113
PushMessage msg;
110-
msg.sub = sub;
114+
msg.subscription = subscription;
111115
msg.payload = "{\"title\":\"Hello\",\"body\":\"ESP32\"}";
112116

113117
// Raw payload strings remain supported, but they are not schema-validated.
@@ -118,60 +122,73 @@ WebPushResult result = webPush.send(msg);
118122

119123
```cpp
120124
if (webPush.isInitialized()) {
121-
webPush.deinit();
125+
WebPushJoinStatus stopStatus = webPush.deinit();
126+
if (stopStatus == WebPushJoinStatus::Timeout) {
127+
ESP_LOGW("WEBPUSH", "Worker did not stop within the timeout");
128+
}
122129
}
123130
```
124131

132+
`requestStop()` marks shutdown and wakes the worker without blocking. `join(timeoutMs)` waits for the worker to exit and finalizes shutdown when the stop completes in time. `deinit(timeoutMs)` is the convenience wrapper that performs both in one call.
133+
125134
## Configuration
126135

127136
`WebPushConfig` lets you tune the worker and queue:
128137

129-
- `queueLength` – number of queued messages.
130-
- `queueMemory``Internal`, `Psram`, or `Any`.
131-
- `worker` – stack size, priority, core id, PSRAM stack usage.
132-
- `requestTimeoutMs` – HTTP timeout.
133-
- `ttlSeconds` – Web Push TTL header.
134-
- `maxRetries`, `retryBaseDelayMs`, `retryMaxDelayMs` – retry/backoff controls.
135-
- `networkValidator` – optional callback for application-defined network readiness checks.
138+
- `queueLength` - number of queued messages.
139+
- `queueMemory` - `Internal`, `Psram`, or `Any`.
140+
- `worker` - stack size, priority, core id, and task name.
141+
- `requestTimeoutMs` - HTTP timeout.
142+
- `ttlSeconds` - Web Push TTL header.
143+
- `maxRetries`, `retryBaseDelayMs`, `retryMaxDelayMs` - retry/backoff controls.
144+
- `maxPayloadBytes` - plaintext payload size guard. The default is 3993 bytes; use `0` to disable.
145+
- `networkValidator` - optional callback for application-defined network readiness checks.
136146

137147
## Gotchas
138-
- **System time is required** for VAPID JWT expiration. Ensure SNTP is synced.
139-
- Web Push endpoints require TLS; `esp_http_client` must be built with TLS support.
140-
- `aesgcm` content encoding is used to match existing Web Push payloads.
141-
- Structured payload inputs reject unknown top-level keys and invalid field types.
142-
143-
## API Reference (Core)
144-
145-
- `bool init(contactEmail, publicKeyBase64, privateKeyBase64, config)`
146-
- `bool send(const PushMessage&, WebPushResultCB cb)` (async)
147-
- `WebPushResult send(const PushMessage&)` (sync)
148-
- `bool send(const Subscription&, const PushPayload&, WebPushResultCB cb)` / `WebPushResult send(const Subscription&, const PushPayload&)`
149-
- `bool send(const Subscription&, const JsonDocument&, WebPushResultCB cb)` / `WebPushResult send(const Subscription&, const JsonDocument&)`
150-
- `bool send(const Subscription&, JsonVariantConst, WebPushResultCB cb)` / `WebPushResult send(const Subscription&, JsonVariantConst)`
148+
- System time is required for VAPID JWT expiration.
149+
- Web Push endpoints require TLS-capable `esp_http_client`.
150+
- Only `aes128gcm` is generated. Legacy `aesgcm` is intentionally not supported in v2.
151+
- `subject` must start with `mailto:` or `https://`.
152+
- The configured VAPID public key must match the private key.
153+
154+
## API Reference
155+
156+
- `bool init(const WebPushVapidConfig&, const WebPushConfig& = {})`
157+
- `WebPushEnqueueResult send(const PushMessage&, WebPushResultCB cb)`
158+
- `WebPushResult send(const PushMessage&)`
159+
- `WebPushEnqueueResult send(const WebPushSubscription&, const PushPayload&, WebPushResultCB cb)`
160+
- `WebPushResult send(const WebPushSubscription&, const PushPayload&)`
161+
- `WebPushEnqueueResult send(const WebPushSubscription&, const JsonDocument&, WebPushResultCB cb)`
162+
- `WebPushResult send(const WebPushSubscription&, const JsonDocument&)`
163+
- `WebPushEnqueueResult send(const WebPushSubscription&, JsonVariantConst, WebPushResultCB cb)`
164+
- `WebPushResult send(const WebPushSubscription&, JsonVariantConst)`
165+
- `void requestStop()`
166+
- `WebPushJoinStatus join(uint32_t timeoutMs)`
151167
- `void setNetworkValidator(WebPushNetworkValidator)`
152-
- `void deinit()` / `bool isInitialized() const`
153-
- `const char* errorToString(WebPushError)`
168+
- `WebPushJoinStatus deinit(uint32_t timeoutMs = 10000)` / `bool isInitialized() const`
169+
- `const char *errorToString(WebPushError)`
154170

155-
## Restrictions
156-
- ESP32-class targets only (Arduino + ESP-IDF).
171+
## Compatibility
172+
- ESP32-class targets only.
173+
- Arduino and ESP-IDF frameworks are supported.
157174
- Requires C++17, ArduinoJson v7+, and mbedTLS.
158175
- Do not call from ISR context.
159176

160177
## Tests
161-
Host-side tests are disabled. Use the `examples/` sketches with PlatformIO or Arduino CLI.
178+
- On-device Unity tests live in `test/test_esp_webPush`.
179+
- CI builds Arduino examples and includes an ESP-IDF compile smoke build.
162180

163181
## Formatting Baseline
164182

165183
This repository follows the firmware formatting baseline from `esptoolkit-template`:
166184
- `.clang-format` is the source of truth for C/C++/INO layout.
167185
- `.editorconfig` enforces tabs (`tab_width = 4`), LF endings, and final newline.
168-
- Format all tracked firmware sources with `bash scripts/format_cpp.sh`.
186+
- Format tracked firmware sources with `bash scripts/format_cpp.sh`.
169187

170188
## License
171-
MIT see [LICENSE.md](LICENSE.md).
189+
MIT - see [LICENSE.md](LICENSE.md).
172190

173191
## ESPToolKit
174192
- Check out other libraries: <https://github.com/orgs/ESPToolKit/repositories>
175-
- Hang out on Discord: <https://discord.gg/WG8sSqAy>
176193
- Support the project: <https://ko-fi.com/esptoolkit>
177194
- Visit the website: <https://www.esptoolkit.hu/>

0 commit comments

Comments
 (0)