feat(approvals): gate push Approve/Deny buttons on the opt-in setting too

One switch now governs the whole feature: with approvalsInboxEnabled off
(the default), sendPushNotifications strips the actions and approvalId
from permission push payloads, so the buttons no longer render at all
(pre-inbox they rendered and did nothing). The page-side action relay is
gated the same way for stale notifications sent before the toggle
flipped. Only the store and answer endpoints keep running, so enabling
the toggle surfaces anything already pending immediately.

sendPushNotifications is async now (cached settings read); all call
sites were already fire-and-forget. Covered by three new payload tests
alongside the existing hostTitle suite.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Codeman maintainer
2026-08-09 16:03:27 +02:00
parent 6c744f8677
commit 338f0e460d
6 changed files with 106 additions and 24 deletions
+18 -3
View File
@@ -2104,13 +2104,27 @@ export class WebServer extends EventEmitter {
* Only events in PUSH_EVENT_MAP trigger push. Per-subscription preferences are checked.
* Expired subscriptions (410/404) are auto-removed.
*/
private sendPushNotifications(event: string, data: Record<string, unknown>): void {
// Async only for the Approvals Inbox settings read below; every call site is
// fire-and-forget (the EventPort signature stays `void`).
private async sendPushNotifications(event: string, data: Record<string, unknown>): Promise<void> {
const template = WebServer.PUSH_EVENT_MAP[event];
if (!template) return;
const subscriptions = this.pushStore.getAll();
if (subscriptions.length === 0) return;
// Approvals Inbox gating: the Approve/Deny action buttons answer through
// the inbox, so both the buttons and the approvalId they act on ship only
// when the OPT-IN `approvalsInboxEnabled` setting is on (default OFF).
// Pre-inbox these buttons rendered and did nothing; stripping them when
// the feature is off is the honest shape. Cheap: the settings read is
// cached (~2s TTL) and only taken for events that carry approval parts.
let approvalsEnabled = false;
if (template.actions || typeof data.approvalId === 'string') {
const settings = await this.readSettings();
approvalsEnabled = settings.approvalsInboxEnabled === true;
}
const vapidKeys = this.pushStore.getVapidKeys();
webpush.setVapidDetails('mailto:codeman@localhost', vapidKeys.publicKey, vapidKeys.privateKey);
@@ -2154,9 +2168,10 @@ export class WebServer extends EventEmitter {
sessionId,
// Approvals Inbox item id: lets sw.js answer an Approve/Deny action
// click directly (POST /api/approvals/:id/answer) with no tab open.
approvalId: typeof data.approvalId === 'string' ? data.approvalId : undefined,
// Gated on the opt-in setting together with the action buttons.
approvalId: approvalsEnabled && typeof data.approvalId === 'string' ? data.approvalId : undefined,
urgency: template.urgency,
actions: template.actions,
actions: approvalsEnabled ? template.actions : undefined,
});
for (const sub of subscriptions) {