refactor(notifications): keep the push sender contract next to the fan-out
The payload, target and outcome types are shared by the Expo and web push senders, so they now live with PushSender in push-notifications.ts instead of in the web push adapter. The settings E2E also stubs the Notification permission (headless Chromium reports it as denied even when granted), and the quiet-hours inputs are wide enough for 12-hour times. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
This commit is contained in:
parent
7185dcdd3c
commit
2d95db70f2
@ -8,9 +8,26 @@ import { captureScreenshot, completeOnboarding, openUserSettings, rpc, signup }
|
||||
|
||||
const ENDPOINT = "https://push.example.test/e2e-device";
|
||||
|
||||
/** A fake service worker registration whose push subscription lives in sessionStorage. */
|
||||
/**
|
||||
* A fake Notification permission and service worker registration, kept in sessionStorage so they
|
||||
* survive a reload. Headless Chromium reports notifications as denied even when granted.
|
||||
*/
|
||||
async function stubPushApi(page: Page, endpoint: string) {
|
||||
await page.addInitScript((pushEndpoint) => {
|
||||
const permissionKey = "e2e:notification-permission";
|
||||
Object.defineProperty(window, "Notification", {
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: class {
|
||||
static get permission() {
|
||||
return sessionStorage.getItem(permissionKey) ?? "default";
|
||||
}
|
||||
static async requestPermission() {
|
||||
sessionStorage.setItem(permissionKey, "granted");
|
||||
return "granted";
|
||||
}
|
||||
},
|
||||
});
|
||||
const storageKey = "e2e:push-subscription";
|
||||
const current = () => {
|
||||
const stored = sessionStorage.getItem(storageKey);
|
||||
@ -59,7 +76,6 @@ async function stubPushApi(page: Page, endpoint: string) {
|
||||
test("notification settings: push on this device, quiet hours and nudges", async ({
|
||||
page,
|
||||
}, testInfo) => {
|
||||
await page.context().grantPermissions(["notifications"]);
|
||||
await stubPushApi(page, ENDPOINT);
|
||||
await signup(page, `notifications-${Date.now()}@rakazo.test`, "password12", "Push Tester");
|
||||
await completeOnboarding(page);
|
||||
|
||||
@ -167,7 +167,7 @@ export function NotificationSettingsPanel() {
|
||||
aria-label={t`Quiet hours start`}
|
||||
value={quietDraft.start}
|
||||
onChange={(event) => changeQuietTime("start", event.target.value)}
|
||||
className="w-32"
|
||||
className="w-36"
|
||||
/>
|
||||
<span aria-hidden="true" className="text-muted-foreground">
|
||||
–
|
||||
@ -178,7 +178,7 @@ export function NotificationSettingsPanel() {
|
||||
aria-label={t`Quiet hours end`}
|
||||
value={quietDraft.end}
|
||||
onChange={(event) => changeQuietTime("end", event.target.value)}
|
||||
className="w-32"
|
||||
className="w-36"
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
@ -10,7 +10,7 @@ import {
|
||||
legacyPushTokenDir,
|
||||
MAX_EXPO_PUSH_RESPONSE_BYTES,
|
||||
} from "./expo-push.js";
|
||||
import type { PushPayload } from "./web-push.js";
|
||||
import type { PushPayload } from "./push-notifications.js";
|
||||
|
||||
const dirs: string[] = [];
|
||||
|
||||
|
||||
@ -5,7 +5,7 @@ import { ExpoPushTokenSchema } from "@rakazo/contracts";
|
||||
import type { PrismaClient } from "@rakazo/db";
|
||||
import { savePushSubscription } from "@rakazo/db";
|
||||
import { combineSignals } from "./connector-safety.js";
|
||||
import type { PushOutcome, PushPayload, PushTarget } from "./web-push.js";
|
||||
import type { PushOutcome, PushPayload, PushTarget } from "./push-notifications.js";
|
||||
import { readBodyCapped, withAbort } from "./web-ssrf.js";
|
||||
|
||||
const O_NOFOLLOW = constants.O_NOFOLLOW ?? 0;
|
||||
|
||||
@ -9,8 +9,11 @@ import {
|
||||
} from "@rakazo/db";
|
||||
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
|
||||
import { notifyUserFromTool } from "./notify-user-tool.js";
|
||||
import { PushNotificationProvider, type PushSender } from "./push-notifications.js";
|
||||
import type { PushPayload } from "./web-push.js";
|
||||
import {
|
||||
PushNotificationProvider,
|
||||
type PushPayload,
|
||||
type PushSender,
|
||||
} from "./push-notifications.js";
|
||||
|
||||
const databaseUrl = process.env.DATABASE_URL;
|
||||
const describePostgres =
|
||||
|
||||
@ -1,8 +1,12 @@
|
||||
import type { NotificationMessage } from "@rakazo/adapter-kit";
|
||||
import type { PrismaClient } from "@rakazo/db";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { PushNotificationProvider, type PushSender } from "./push-notifications.js";
|
||||
import type { PushOutcome, PushPayload } from "./web-push.js";
|
||||
import {
|
||||
PushNotificationProvider,
|
||||
type PushOutcome,
|
||||
type PushPayload,
|
||||
type PushSender,
|
||||
} from "./push-notifications.js";
|
||||
|
||||
const context = {
|
||||
operationId: "notify",
|
||||
|
||||
@ -11,11 +11,24 @@ import {
|
||||
markPushSubscriptionUsed,
|
||||
} from "@rakazo/db";
|
||||
import { getLogger } from "@rakazo/logging";
|
||||
import type { PushOutcome, PushPayload, PushTarget } from "./web-push.js";
|
||||
|
||||
/** Lock screens show a line or two; the rest lives in the conversation. */
|
||||
const PUSH_BODY_MAX_LENGTH = 240;
|
||||
|
||||
/** What a device shows: the notification plus where a tap leads (a path in the web app). */
|
||||
export interface PushPayload extends NotificationMessage {
|
||||
url: string;
|
||||
}
|
||||
|
||||
/** "gone": the push service no longer knows this device, so its registration can be dropped. */
|
||||
export type PushOutcome = "sent" | "gone";
|
||||
|
||||
export interface PushTarget {
|
||||
endpoint: string;
|
||||
p256dh: string | null;
|
||||
auth: string | null;
|
||||
}
|
||||
|
||||
export interface PushSender {
|
||||
send(target: PushTarget, payload: PushPayload, signal?: AbortSignal): Promise<PushOutcome>;
|
||||
}
|
||||
|
||||
@ -2,8 +2,9 @@ import { createECDH, randomBytes } from "node:crypto";
|
||||
import type { PrismaClient } from "@rakazo/db";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import webPush from "web-push";
|
||||
import type { PushPayload } from "./push-notifications.js";
|
||||
import { EncryptedSecretStore } from "./secrets.js";
|
||||
import { type PushPayload, VapidKeys, vapidEnvFromProcess, WebPushSender } from "./web-push.js";
|
||||
import { VapidKeys, vapidEnvFromProcess, WebPushSender } from "./web-push.js";
|
||||
|
||||
const browserKey = createECDH("prime256v1");
|
||||
browserKey.generateKeys();
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
import { lookup } from "node:dns/promises";
|
||||
import type { NotificationMessage } from "@rakazo/adapter-kit";
|
||||
import type { PrismaClient } from "@rakazo/db";
|
||||
import { getLogger } from "@rakazo/logging";
|
||||
import { Agent } from "undici";
|
||||
import webPush from "web-push";
|
||||
import { combineSignals } from "./connector-safety.js";
|
||||
import { createAddressCheckedLookup, type ResolveHostname } from "./network-address.js";
|
||||
import type { PushOutcome, PushPayload, PushTarget } from "./push-notifications.js";
|
||||
import type { EncryptedSecretStore } from "./secrets.js";
|
||||
import { dispatcherFetch } from "./undici-fetch.js";
|
||||
import { assertPublicAddresses, assertSafeWebUrl } from "./web-ssrf.js";
|
||||
@ -137,20 +137,6 @@ export class VapidKeys {
|
||||
}
|
||||
}
|
||||
|
||||
/** What a device shows: the notification plus where a tap leads (a path in the web app). */
|
||||
export interface PushPayload extends NotificationMessage {
|
||||
url: string;
|
||||
}
|
||||
|
||||
/** "gone": the push service no longer knows this device, so its registration can be dropped. */
|
||||
export type PushOutcome = "sent" | "gone";
|
||||
|
||||
export interface PushTarget {
|
||||
endpoint: string;
|
||||
p256dh: string | null;
|
||||
auth: string | null;
|
||||
}
|
||||
|
||||
export interface WebPushSenderOptions {
|
||||
fetch?: typeof globalThis.fetch;
|
||||
resolveHostname?: ResolveHostname;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user