feat(push): owner-only gate and subscription body validation (T10)
isOwner compares the session email to the configured owner; the body matches the browser PushSubscription.toJSON() shape, reporter resolved server-side.
This commit is contained in:
7
server/utils/isOwner.ts
Normal file
7
server/utils/isOwner.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
// Owner-only gate for Web Push (T10). v1 notifies a single configured owner
|
||||
// (DESIGN G3); the owner email comes from runtime config server-side. If no
|
||||
// owner is configured, nobody qualifies — push enrolment stays closed.
|
||||
export function isOwner(email: string | undefined, ownerEmail: string | undefined): boolean {
|
||||
if (!email || !ownerEmail) return false
|
||||
return email.toLowerCase() === ownerEmail.toLowerCase()
|
||||
}
|
||||
16
server/utils/pushSubscriptionInput.ts
Normal file
16
server/utils/pushSubscriptionInput.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { type } from 'arktype'
|
||||
|
||||
// Validated push-subscription body — the browser's PushSubscription.toJSON()
|
||||
// shape (T10/F8). Never trusted from the client beyond these constraints; the
|
||||
// reporter is resolved server-side from the session, not sent here.
|
||||
const nonEmptyKey = type('string').narrow((s, ctx) => s.length > 0 || ctx.reject('a non-empty key'))
|
||||
|
||||
export const PushSubscriptionInput = type({
|
||||
endpoint: 'string.url',
|
||||
keys: {
|
||||
p256dh: nonEmptyKey,
|
||||
auth: nonEmptyKey,
|
||||
},
|
||||
})
|
||||
|
||||
export type PushSubscriptionInputData = typeof PushSubscriptionInput.infer
|
||||
Reference in New Issue
Block a user