Review of the notification actions. With a plugin that may stop a move
or read change, or a silent tab holding the login, the message now
opens instead of the route acting. The route answers 401 under a
session limit, and neither path touches a message since moved to Trash
or Junk. A tab that thawed declines an old click, Archive follows the
gate of the notification's login and files into a shared-out Archive
as the app's button does, Reply opens a composer in the Pro shell, the
route retries once when another client made the folder first and
leaves no empty folder behind, and logins match however the server
address is written.
Two assertions of this feature's own tests change: the route answers
401, not 403, when a login's scopes can't be worked out, since that now
also covers session limits; a tab's message to the page now carries
`at`.
The Pro shell's mail surface now reads a parked link's query from the
handoff: the address bar may have none by its first render, which lost
`?slot=` and `?owner=` there too.
A notification for one message gets buttons where the browser has room
for them, and a click on its body works as before. The service worker
has no login, so an open tab takes the action first, through the store
actions. With no tab open a new same-origin route, /api/push/action,
does Mark read and Archive with the context cookie. When the cookie is
gone, the token has expired or the login can't be told apart, the
message opens in the app instead. Reply opens the composer through a
`?reply=1` link, with any text typed on the notification.
- `public/sw.js`: buttons, asking open tabs, the route, `sync` handling
- `app/api/push/action`: same-origin, checks the login and the account,
honours the admin's Archive gate, files by the mirrored archive mode
- `lib/pwa-notify`: the page's side, the mirror for the worker, and
`requestOutboxSync`, which the offline queue calls
- Background Sync, thin version: a `sync` event tells open tabs to flush
Two push-preview-route assertions change: they match the whole answer
for a named message, which now also carries `login`, what the action
route checks so a stale notification can't act on whoever holds the
cookie slot now.
Settings > Backup & reset gets "Keep mail on this device", off until the
user turns it on in this browser. While it is on:
- the JMAP client hands the server's answers to an encrypted cache in
IndexedDB (AES-GCM, non-extractable key) and answers from it when the
network is gone: the folder list, the last 30 days of the Inbox and of
the folders the user opened, bodies and attachments, at most 250 MB,
oldest out first; a background sync keeps it current
- read, flag, tags and moves made offline are queued and sent in order
when the server answers again; deleting for good and emptying a folder
are refused offline
- the composer, quick reply and the S/MIME and PGP plugins' sends that
complete the composer's go to an outbox once every step up to the
transport ran (checks, label, signature, plugin send checks); only the
finished send waits, with files attached offline
- the service worker keeps this build's app shell, so the app also
starts offline from the kept session and the last policy
- a full sign-out, the idle sign-out and signing one account out delete
it, a sign-out the user starts asks first when mail is unsent, and the
login page names what an automatic sign-out deleted; a policy fetch
that forbids it (devices.noOffline) wipes that account
Not in Lite.
The push preview route now reads the device policy of the login it
serves (its scopes worked out from the login's own JMAP session, every
non-personal account counted as a group) and under
`pushWithoutContent` asks the mail server for the message's ids only,
so sender, subject and text never reach the notification. The service
worker shows "New mail" for such an answer. A policy that can't be read
counts as on.
The three existing push preview route tests now mock the config manager
with the default policy: without it the route can't read a policy in the
test environment and holds the text back, which those tests didn't mean
to cover. No assertion changed.
With `OTEL_EXPORTER_OTLP_ENDPOINT` or
`OTEL_EXPORTER_OTLP_TRACES_ENDPOINT` set (and neither
`OTEL_SDK_DISABLED=true` nor `OTEL_TRACES_EXPORTER=none`), Bulwark
loads the OpenTelemetry SDK at startup and exports Next's own spans
plus a client span for every call to Stalwart, WOPI, translation, the
plugin proxy and OAuth over OTLP/HTTP. Calls to Stalwart carry a
`traceparent` header, so Stalwart's spans join the trace. All other
settings come from the standard `OTEL_*` variables.
Every span is scrubbed before export: URLs keep their origin only,
request paths become their route group, query strings, user agents and
client addresses are dropped, and addresses or credentials in other
attributes are removed. Without an endpoint the SDK is never loaded.
The endpoint is listed under the hosts that stay reachable with no
third-party traffic, and System > Monitoring shows whether tracing runs.
A second switch under System > Usage stats, "Also send anonymous error
counts", or `BULWARK_TELEMETRY_ERRORS=on|off`. Off by default, off
whenever the heartbeat is off or no third-party traffic is on, and
switching the heartbeat off switches it off too. With it on the
heartbeat becomes schema 2: the v1 fields unchanged plus `errors`, up to
50 fingerprints of the last complete UTC day with the error name, the
top frames in Bulwark's bundles (path, line, column, debug ID), the
version, a bucketed count, on how many of the last 7 days it occurred,
and a message only when it is a fixed error code. Without the consent
the heartbeat stays exactly schema 1. The admin preview shows the v2
payload as soon as the switch is flipped.
- `/api/metrics` in Prometheus text format, off until `METRICS_TOKEN`
or `METRICS_TOKEN_FILE` is set (404 before, 401 without the bearer
token): requests per route group with status class and latency,
sign-in outcomes, calls to Stalwart, WOPI, translation, the plugin
proxy and OAuth, browser errors, errors today, process memory,
event-loop lag, uptime and build info. Labels come from fixed lists.
Each replica serves its own.
- `/api/health/live` answers while the process runs;
`/api/health/ready` checks the config, the shared store, writable
data dirs and Stalwart's session resource. `/api/health` and
`/api/ready` are unchanged.
- the Docker image gets a HEALTHCHECK on the live endpoint
- adds prom-client, and the OpenTelemetry packages the tracing step
loads on demand
Admins had logs and nothing else: client errors only reached the
browser console. An admin can now set a Sentry-protocol address (DSN)
for Bugsink, GlitchTip or Sentry under System > Monitoring, or with
`ERROR_REPORTING_DSN`, `ERROR_REPORTING_DSN_FILE`,
`ERROR_REPORTING_SAMPLE_RATE` and `ERROR_REPORTING_ENVIRONMENT`.
Bulwark Lite reads the same from `errorReporting` in policy.json.
Everything stays off while no address is set.
- browsers report errors from the error boundaries, window.onerror,
unhandled rejections and failed JMAP requests (method and error type
only), with route and JMAP method breadcrumbs and the chunks' debug IDs
- the server reports what Next hands to onRequestError and what routes
log with logger.error
- every report is scrubbed first: quoted text, addresses, URLs, file
names, tokens and the rest after the first line are removed, and the
server scrubs what browsers post to /api/errors again
- reports are deduplicated and rate-limited per session and process
- errors are counted per day for "errors today" on the admin page
- startup messages and audit entries go through the logger, so
`LOG_FORMAT=json` covers them
directory-keys-shared.test.ts: the store-down case now expects the store error instead of an 'invalid' signature, since a store outage no longer counts as a bad signature.
The marker-write test from d28ee361c now expects the 503 path (a rejected promise) instead of false: refusing with "Token already used" sent admins looking for a replay that never happened, and the jti is now remembered in memory only after it was recorded, so a retry works once the store or state dir is back.