notifications(P4f): audit fixes, lock down 047 + best-effort enqueue paths
Two findings from an ultrathink audit of the Phase 4 work. (1) members_with_permission was callable by any PostgREST client. Postgres grants EXECUTE on new functions to PUBLIC by default, and this codebase has no existing REVOKE pattern. The function returns a SET of member_ids for any (space, permission) pair, so without a lockdown ANY authenticated caller could enumerate "who has permission X in space Y" for spaces they are not even members of, learning member_ids and the permission distribution. user_has_permission has a narrower exposure (per-uid boolean) and is needed by anon/authenticated for RLS-policy evaluation, so it stays callable; the new function is locked to service_role since the only legitimate caller (the form-submission fan-out) goes through the admin client. Migration 047 + schema.sql now REVOKE EXECUTE FROM PUBLIC and GRANT TO service_role. (2) The Phase 4 enqueue blocks in equipment.ts (reserve, cancel) and classes.ts (signUp, cancelMySignup waitlist promotion, updateSession session-cancellation fan-out) used the shared helpers, but the helpers themselves were not internally best-effort. The Stripe webhook's original enqueueDues wrapped its entire body in try/catch so a thrown member-lookup error could never bubble into the money path; my refactor removed that inner wrap and relied on enqueueNotification's best-effort. resolveMemberContact and getSpaceName could still throw on a transient supabase network error, which would surface to the action return value after the underlying mutation already committed: the client would see an error even though their booking, signup, or session-cancellation succeeded. Fixed at both layers: the helpers now swallow throws internally (returning null / empty), and each call site in equipment/classes is wrapped in try/catch around the lookup chain so secondary action-specific queries (equipment name+location lookup, promoted-signup session lookup, session-cancellation signup fan-out) also cannot leak. forms.submitForm already had the wrap from P4d. 2 new helper tests pin the best-effort behavior; suite 567, build clean.
This commit is contained in:
@@ -15,6 +15,7 @@ function mockAdmin(opts: {
|
||||
selectResult?: { data: Row | null; error?: { message: string } | null }
|
||||
upsertResult?: { error: { message: string } | null }
|
||||
throwOnUpsert?: boolean
|
||||
throwOnMaybeSingle?: boolean
|
||||
}) {
|
||||
const upsert = vi.fn(async () => {
|
||||
if (opts.throwOnUpsert) throw new Error('boom')
|
||||
@@ -24,9 +25,10 @@ function mockAdmin(opts: {
|
||||
const builder = {
|
||||
select: vi.fn().mockReturnThis(),
|
||||
eq: vi.fn().mockReturnThis(),
|
||||
maybeSingle: vi.fn(async () =>
|
||||
opts.selectResult ?? { data: null, error: null },
|
||||
),
|
||||
maybeSingle: vi.fn(async () => {
|
||||
if (opts.throwOnMaybeSingle) throw new Error('network down')
|
||||
return opts.selectResult ?? { data: null, error: null }
|
||||
}),
|
||||
upsert,
|
||||
}
|
||||
const from = vi.fn(() => builder)
|
||||
@@ -62,6 +64,14 @@ describe('resolveMemberContact', () => {
|
||||
const r = await resolveMemberContact(admin as never, 'space-1', 'member-1')
|
||||
expect(r).toEqual({ email: null, displayName: null })
|
||||
})
|
||||
|
||||
it('returns null and never throws when the lookup itself throws (best-effort)', async () => {
|
||||
const admin = mockAdmin({ throwOnMaybeSingle: true })
|
||||
const spy = vi.spyOn(console, 'error').mockImplementation(() => {})
|
||||
await expect(resolveMemberContact(admin as never, 'space-1', 'member-1')).resolves.toBeNull()
|
||||
expect(spy).toHaveBeenCalled()
|
||||
spy.mockRestore()
|
||||
})
|
||||
})
|
||||
|
||||
describe('enqueueNotification', () => {
|
||||
@@ -155,4 +165,11 @@ describe('getSpaceName', () => {
|
||||
const admin = mockAdmin({ selectResult: { data: null, error: null } })
|
||||
expect(await getSpaceName(admin as never, 'ghost')).toBe('')
|
||||
})
|
||||
it('returns empty string when the lookup itself throws (best-effort)', async () => {
|
||||
const admin = mockAdmin({ throwOnMaybeSingle: true })
|
||||
const spy = vi.spyOn(console, 'error').mockImplementation(() => {})
|
||||
await expect(getSpaceName(admin as never, 'space-1')).resolves.toBe('')
|
||||
expect(spy).toHaveBeenCalled()
|
||||
spy.mockRestore()
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user