Skip to content

Commit 899a5f1

Browse files
committed
feat(webhook): add X-Webhook-Source header to indicate event origin
Derive source (admin/visitor/system) from EventScope and include in webhook headers. Update @mx-space/webhook package types and handler.
1 parent 0d52052 commit 899a5f1

3 files changed

Lines changed: 38 additions & 5 deletions

File tree

apps/core/src/modules/webhook/webhook.service.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,18 @@ import { WebhookEventModel } from './webhook-event.model'
2020

2121
const ACCEPT_EVENTS = new Set(Object.values(BusinessEvents))
2222

23+
type WebhookEventSource = 'admin' | 'visitor' | 'system'
24+
25+
function scopeToSource(scope: EventScope): WebhookEventSource {
26+
const hasVisitor = (scope & EventScope.TO_VISITOR) !== 0
27+
const hasAdmin = (scope & EventScope.TO_ADMIN) !== 0
28+
29+
if (hasVisitor && !hasAdmin) return 'admin'
30+
if (hasAdmin && !hasVisitor) return 'visitor'
31+
if (hasVisitor && hasAdmin) return 'admin'
32+
return 'system'
33+
}
34+
2335
@Injectable()
2436
export class WebhookService implements OnModuleInit, OnModuleDestroy {
2537
constructor(
@@ -120,9 +132,11 @@ export class WebhookService implements OnModuleInit, OnModuleDestroy {
120132
rawPayload,
121133
)
122134

135+
const source = scopeToSource(scope)
136+
123137
await Promise.all(
124138
scopedWebhooks.map((webhook) => {
125-
return this.sendWebhookEvent(event, payload, webhook)
139+
return this.sendWebhookEvent(event, payload, webhook, source)
126140
}),
127141
)
128142
}
@@ -131,6 +145,7 @@ export class WebhookService implements OnModuleInit, OnModuleDestroy {
131145
event: string,
132146
payload: object,
133147
webhook: WebhookModel,
148+
source: WebhookEventSource = 'system',
134149
) {
135150
const stringifyPayload = JSON.stringify(payload)
136151
const clonedPayload = JSON.parse(stringifyPayload)
@@ -147,6 +162,7 @@ export class WebhookService implements OnModuleInit, OnModuleDestroy {
147162
webhook.secret,
148163
stringifyPayload,
149164
),
165+
'X-Webhook-Source': source,
150166
}
151167
const webhookEvent = await this.webhookEventModel.create({
152168
event,

packages/webhook/src/handler.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,14 @@ import assert from 'node:assert'
22
import { createHmac, timingSafeEqual } from 'node:crypto'
33
import { EventEmitter } from 'node:events'
44
import type { IncomingMessage, ServerResponse } from 'node:http'
5+
56
import { InvalidSignatureError } from './error'
67
import type { BusinessEvents } from './event.enum'
7-
import type { ExtendedEventEmitter, GenericEvent } from './types'
8+
import type {
9+
ExtendedEventEmitter,
10+
GenericEvent,
11+
WebhookEventSource,
12+
} from './types'
813

914
interface CreateHandlerOptions {
1015
secret: string
@@ -24,12 +29,13 @@ export const createHandler = (options: CreateHandlerOptions): Handler => {
2429
try {
2530
const data = await readDataFromRequest({ req, secret })
2631

27-
const { type: event, payload } = data
32+
const { type: event, payload, source } = data
2833

29-
handler.emitter.emit(event as any, payload)
34+
handler.emitter.emit(event as any, payload, source)
3035
handler.emitter.emit('*', {
3136
type: event,
3237
payload,
38+
source,
3339
})
3440
res.statusCode = 200
3541
res.setHeader('Content-Type', 'application/json')
@@ -74,10 +80,14 @@ export const readDataFromRequest = async ({
7480
verifyWebhook(secret, stringifyPayload, signature256 as string) &&
7581
verifyWebhookSha1(secret, stringifyPayload, signature as string)
7682

83+
const source = (req.headers['x-webhook-source'] ||
84+
'system') as WebhookEventSource
85+
7786
if (isValid) {
7887
return {
7988
type: event as BusinessEvents,
8089
payload: obj as any,
90+
source,
8191
} as GenericEvent
8292
} else {
8393
console.error('revice a invalidate webhook payload', req.headers)

packages/webhook/src/types.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { EventEmitter } from 'node:events'
2+
23
import type { CommentModel } from '@core/modules/comment/comment.model'
34
import type { LinkModel } from '@core/modules/link/link.model'
45
import type { NoteModel } from '@core/modules/note/note.model'
@@ -8,14 +9,19 @@ import type { PostModel } from '@core/modules/post/post.model'
89
import type { NormalizedPost } from '@core/modules/post/post.type'
910
import type { RecentlyModel } from '@core/modules/recently/recently.model'
1011
import type { SayModel } from '@core/modules/say/say.model'
12+
1113
import type { ReaderModel } from '~/modules/reader/reader.model'
14+
1215
import type { BusinessEvents } from './event.enum'
1316

17+
export type WebhookEventSource = 'admin' | 'visitor' | 'system'
18+
1419
export interface ExtendedEventEmitter extends EventEmitter {
1520
on: (<T extends BusinessEvents>(
1621
event: T,
1722
listener: (
1823
data: EventPayloadMapping[Extract<T, keyof EventPayloadMapping>],
24+
source: WebhookEventSource,
1925
) => void,
2026
) => this) &
2127
((event: '*', listener: (event: GenericEvent) => void) => this)
@@ -76,7 +82,7 @@ export interface IActivityLike {
7682
}
7783

7884
// Auto Generaged type.
79-
export type GenericEvent =
85+
export type GenericEvent = (
8086
| { type: BusinessEvents.POST_CREATE; payload: NormalizedPost }
8187
| { type: BusinessEvents.POST_UPDATE; payload: NormalizedPost }
8288
| { type: BusinessEvents.POST_DELETE; payload: PayloadOnlyId }
@@ -115,3 +121,4 @@ export type GenericEvent =
115121
}
116122
}
117123
| { type: 'health_check'; payload: {} }
124+
) & { source: WebhookEventSource }

0 commit comments

Comments
 (0)