@apso/domain-events 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +68 -0
- package/dist/destinations/delivery-destination.d.ts +25 -0
- package/dist/destinations/delivery-destination.d.ts.map +1 -0
- package/dist/destinations/delivery-destination.js +10 -0
- package/dist/destinations/delivery-destination.js.map +1 -0
- package/dist/destinations/eventbridge.destination.d.ts +23 -0
- package/dist/destinations/eventbridge.destination.d.ts.map +1 -0
- package/dist/destinations/eventbridge.destination.js +55 -0
- package/dist/destinations/eventbridge.destination.js.map +1 -0
- package/dist/destinations/index.d.ts +22 -0
- package/dist/destinations/index.d.ts.map +1 -0
- package/dist/destinations/index.js +60 -0
- package/dist/destinations/index.js.map +1 -0
- package/dist/destinations/kafka.destination.d.ts +24 -0
- package/dist/destinations/kafka.destination.d.ts.map +1 -0
- package/dist/destinations/kafka.destination.js +66 -0
- package/dist/destinations/kafka.destination.js.map +1 -0
- package/dist/destinations/sqs.destination.d.ts +20 -0
- package/dist/destinations/sqs.destination.d.ts.map +1 -0
- package/dist/destinations/sqs.destination.js +46 -0
- package/dist/destinations/sqs.destination.js.map +1 -0
- package/dist/destinations/webhook.destination.d.ts +34 -0
- package/dist/destinations/webhook.destination.d.ts.map +1 -0
- package/dist/destinations/webhook.destination.js +73 -0
- package/dist/destinations/webhook.destination.js.map +1 -0
- package/dist/domain-event.entity.d.ts +28 -0
- package/dist/domain-event.entity.d.ts.map +1 -0
- package/dist/domain-event.entity.js +62 -0
- package/dist/domain-event.entity.js.map +1 -0
- package/dist/domain-event.mapper.d.ts +41 -0
- package/dist/domain-event.mapper.d.ts.map +1 -0
- package/dist/domain-event.mapper.js +42 -0
- package/dist/domain-event.mapper.js.map +1 -0
- package/dist/domain-event.relay.d.ts +80 -0
- package/dist/domain-event.relay.d.ts.map +1 -0
- package/dist/domain-event.relay.js +167 -0
- package/dist/domain-event.relay.js.map +1 -0
- package/dist/domain-event.subscriber.d.ts +33 -0
- package/dist/domain-event.subscriber.d.ts.map +1 -0
- package/dist/domain-event.subscriber.js +98 -0
- package/dist/domain-event.subscriber.js.map +1 -0
- package/dist/domain-events.module.d.ts +43 -0
- package/dist/domain-events.module.d.ts.map +1 -0
- package/dist/domain-events.module.js +74 -0
- package/dist/domain-events.module.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +23 -0
- package/dist/index.js.map +1 -0
- package/package.json +46 -0
package/README.md
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# @apso/domain-events
|
|
2
|
+
|
|
3
|
+
Durable **domain-event spine** for NestJS + TypeORM — the standard
|
|
4
|
+
**transactional outbox** pattern, surfaced with generic domain-event naming.
|
|
5
|
+
|
|
6
|
+
State changes to opted-in entities write a `DomainEvent` row **in the same
|
|
7
|
+
transaction**; a self-contained relay drains pending rows and fans them out to
|
|
8
|
+
runtime-selected delivery destinations.
|
|
9
|
+
|
|
10
|
+
See the cross-language [`CONTRACT.md`](../../../CONTRACT.md) for the behavior all
|
|
11
|
+
language implementations honor (TypeScript is the reference).
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install @apso/domain-events
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Peer deps: `@nestjs/common`, `@nestjs/core`, `@nestjs/typeorm`, `typeorm`,
|
|
20
|
+
`rxjs`. Broker SDKs (`@nestjs/microservices` + `kafkajs`, `@aws-sdk/client-sqs`,
|
|
21
|
+
`@aws-sdk/client-eventbridge`) are **optional** and loaded lazily — a
|
|
22
|
+
webhook-only deployment needs none of them.
|
|
23
|
+
|
|
24
|
+
## Wire it up
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
import { DomainEventsModule } from '@apso/domain-events';
|
|
28
|
+
import { Product } from './product/product.entity';
|
|
29
|
+
|
|
30
|
+
@Module({
|
|
31
|
+
imports: [
|
|
32
|
+
DomainEventsModule.forRoot({
|
|
33
|
+
entities: [Product], // the CLI-emitted manifest of opted-in entities
|
|
34
|
+
// mapper?: MyDomainEventMapper, // optional override
|
|
35
|
+
// pollIntervalMs?: 5000, // optional, default 5000
|
|
36
|
+
}),
|
|
37
|
+
],
|
|
38
|
+
})
|
|
39
|
+
export class AppModule {}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
`forRoot` is `@Global()`. The relay starts its own `setInterval` drain on
|
|
43
|
+
application bootstrap and clears it on shutdown — **no external scheduler
|
|
44
|
+
dependency**.
|
|
45
|
+
|
|
46
|
+
## Delivery destinations
|
|
47
|
+
|
|
48
|
+
Active destinations are chosen ENTIRELY at runtime from `EVENTS_DESTINATION`
|
|
49
|
+
(comma-separated → fan-out). Unknown name → startup error.
|
|
50
|
+
|
|
51
|
+
| name | env | dependency |
|
|
52
|
+
|---|---|---|
|
|
53
|
+
| `webhook` | `EVENTS_WEBHOOK_URL`, `EVENTS_WEBHOOK_SECRET` | none (native fetch + crypto) |
|
|
54
|
+
| `kafka` | `EVENTS_KAFKA_BROKERS`, `EVENTS_KAFKA_TOPIC` | `@nestjs/microservices` + `kafkajs` (lazy) |
|
|
55
|
+
| `sqs` | `AWS_REGION`, `EVENTS_SQS_QUEUE_URL` | `@aws-sdk/client-sqs` (lazy) |
|
|
56
|
+
| `eventbridge` | `AWS_REGION`, `EVENTS_EVENTBRIDGE_BUS` | `@aws-sdk/client-eventbridge` (lazy) |
|
|
57
|
+
|
|
58
|
+
The webhook adapter signs with [Standard Webhooks](https://www.standardwebhooks.com/)
|
|
59
|
+
(`webhook-id`, `webhook-timestamp`, `webhook-signature: v1,<sig>`).
|
|
60
|
+
|
|
61
|
+
## Duplicates — consumer-side dedupe is MANDATORY with multiple destinations
|
|
62
|
+
|
|
63
|
+
Delivery is at-least-once and tracked at the **event grain** (a single
|
|
64
|
+
`events.status`), NOT per (event × destination). With more than one active
|
|
65
|
+
destination, a failure in any one destination retries the **whole** event, so
|
|
66
|
+
healthy destinations receive it **again** on every retry. Consumers MUST dedupe
|
|
67
|
+
on `event.id` whenever multiple destinations are active. (A single destination —
|
|
68
|
+
the common case — never duplicates beyond ordinary at-least-once.)
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { DomainEvent } from '../domain-event.entity';
|
|
2
|
+
/**
|
|
3
|
+
* DI token under which the active set of {@link DeliveryDestination} adapters is
|
|
4
|
+
* provided. The {@link DomainEventRelay} injects this (optionally) and fans each
|
|
5
|
+
* event out to every active destination.
|
|
6
|
+
*/
|
|
7
|
+
export declare const DOMAIN_EVENT_DESTINATIONS = "DOMAIN_EVENT_DESTINATIONS";
|
|
8
|
+
/**
|
|
9
|
+
* A DeliveryDestination is a pure transport: it takes a fully-formed
|
|
10
|
+
* {@link DomainEvent} and pushes it to one external sink (a webhook URL, a Kafka
|
|
11
|
+
* topic, an SQS queue, an EventBridge bus, ...).
|
|
12
|
+
*
|
|
13
|
+
* Adapters carry NO application semantics — the event type and payload are set
|
|
14
|
+
* upstream by the {@link DomainEventMapper}. An adapter MUST throw on failure so
|
|
15
|
+
* the relay retries (at-least-once delivery; consumers dedupe on `event.id`).
|
|
16
|
+
*/
|
|
17
|
+
export interface DeliveryDestination {
|
|
18
|
+
/** The destination's stable name, matching its `EVENTS_DESTINATION` token. */
|
|
19
|
+
readonly name: string;
|
|
20
|
+
/**
|
|
21
|
+
* Deliver a single event. Throw on any non-success so the relay retries.
|
|
22
|
+
*/
|
|
23
|
+
send(event: DomainEvent): Promise<void>;
|
|
24
|
+
}
|
|
25
|
+
//# sourceMappingURL=delivery-destination.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"delivery-destination.d.ts","sourceRoot":"","sources":["../../src/destinations/delivery-destination.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AAErD;;;;GAIG;AACH,eAAO,MAAM,yBAAyB,8BAA8B,CAAC;AAErE;;;;;;;;GAQG;AACH,MAAM,WAAW,mBAAmB;IAClC,8EAA8E;IAC9E,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAEtB;;OAEG;IACH,IAAI,CAAC,KAAK,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACzC"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DOMAIN_EVENT_DESTINATIONS = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* DI token under which the active set of {@link DeliveryDestination} adapters is
|
|
6
|
+
* provided. The {@link DomainEventRelay} injects this (optionally) and fans each
|
|
7
|
+
* event out to every active destination.
|
|
8
|
+
*/
|
|
9
|
+
exports.DOMAIN_EVENT_DESTINATIONS = 'DOMAIN_EVENT_DESTINATIONS';
|
|
10
|
+
//# sourceMappingURL=delivery-destination.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"delivery-destination.js","sourceRoot":"","sources":["../../src/destinations/delivery-destination.ts"],"names":[],"mappings":";;;AAEA;;;;GAIG;AACU,QAAA,yBAAyB,GAAG,2BAA2B,CAAC"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { DomainEvent } from '../domain-event.entity';
|
|
2
|
+
import { DeliveryDestination } from './delivery-destination';
|
|
3
|
+
/**
|
|
4
|
+
* EventBridgeDestination puts each domain event onto an Amazon EventBridge bus.
|
|
5
|
+
*
|
|
6
|
+
* The AWS SDK is loaded LAZILY (require) so it is only needed when this
|
|
7
|
+
* destination is actually activated at runtime (EVENTS_DESTINATION=eventbridge).
|
|
8
|
+
* Install it in the deploying service: `npm install @aws-sdk/client-eventbridge`.
|
|
9
|
+
*
|
|
10
|
+
* Each event is put with `Source='apso.domain-events'` and
|
|
11
|
+
* `DetailType=event.type`.
|
|
12
|
+
*
|
|
13
|
+
* Env:
|
|
14
|
+
* AWS_REGION — AWS region (required)
|
|
15
|
+
* EVENTS_EVENTBRIDGE_BUS — target event bus name (required)
|
|
16
|
+
*/
|
|
17
|
+
export declare class EventBridgeDestination implements DeliveryDestination {
|
|
18
|
+
readonly name = "eventbridge";
|
|
19
|
+
private client?;
|
|
20
|
+
private load;
|
|
21
|
+
send(event: DomainEvent): Promise<void>;
|
|
22
|
+
}
|
|
23
|
+
//# sourceMappingURL=eventbridge.destination.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"eventbridge.destination.d.ts","sourceRoot":"","sources":["../../src/destinations/eventbridge.destination.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AACrD,OAAO,EAAE,mBAAmB,EAAE,MAAM,wBAAwB,CAAC;AAE7D;;;;;;;;;;;;;GAaG;AACH,qBAAa,sBAAuB,YAAW,mBAAmB;IAChE,QAAQ,CAAC,IAAI,iBAAiB;IAG9B,OAAO,CAAC,MAAM,CAAC,CAAM;IAGrB,OAAO,CAAC,IAAI;IAYN,IAAI,CAAC,KAAK,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC;CAsB9C"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.EventBridgeDestination = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* EventBridgeDestination puts each domain event onto an Amazon EventBridge bus.
|
|
6
|
+
*
|
|
7
|
+
* The AWS SDK is loaded LAZILY (require) so it is only needed when this
|
|
8
|
+
* destination is actually activated at runtime (EVENTS_DESTINATION=eventbridge).
|
|
9
|
+
* Install it in the deploying service: `npm install @aws-sdk/client-eventbridge`.
|
|
10
|
+
*
|
|
11
|
+
* Each event is put with `Source='apso.domain-events'` and
|
|
12
|
+
* `DetailType=event.type`.
|
|
13
|
+
*
|
|
14
|
+
* Env:
|
|
15
|
+
* AWS_REGION — AWS region (required)
|
|
16
|
+
* EVENTS_EVENTBRIDGE_BUS — target event bus name (required)
|
|
17
|
+
*/
|
|
18
|
+
class EventBridgeDestination {
|
|
19
|
+
constructor() {
|
|
20
|
+
this.name = 'eventbridge';
|
|
21
|
+
}
|
|
22
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
23
|
+
load() {
|
|
24
|
+
try {
|
|
25
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires, global-require
|
|
26
|
+
return require('@aws-sdk/client-eventbridge');
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
throw new Error("EVENTS_DESTINATION=eventbridge requires '@aws-sdk/client-eventbridge' — run " +
|
|
30
|
+
'`npm install @aws-sdk/client-eventbridge`');
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
async send(event) {
|
|
34
|
+
const busName = process.env.EVENTS_EVENTBRIDGE_BUS;
|
|
35
|
+
if (!busName) {
|
|
36
|
+
throw new Error('EVENTS_EVENTBRIDGE_BUS is not set');
|
|
37
|
+
}
|
|
38
|
+
const { EventBridgeClient, PutEventsCommand } = this.load();
|
|
39
|
+
if (!this.client) {
|
|
40
|
+
this.client = new EventBridgeClient({ region: process.env.AWS_REGION });
|
|
41
|
+
}
|
|
42
|
+
await this.client.send(new PutEventsCommand({
|
|
43
|
+
Entries: [
|
|
44
|
+
{
|
|
45
|
+
EventBusName: busName,
|
|
46
|
+
Source: 'apso.domain-events',
|
|
47
|
+
DetailType: event.type,
|
|
48
|
+
Detail: JSON.stringify(event),
|
|
49
|
+
},
|
|
50
|
+
],
|
|
51
|
+
}));
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
exports.EventBridgeDestination = EventBridgeDestination;
|
|
55
|
+
//# sourceMappingURL=eventbridge.destination.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"eventbridge.destination.js","sourceRoot":"","sources":["../../src/destinations/eventbridge.destination.ts"],"names":[],"mappings":";;;AAGA;;;;;;;;;;;;;GAaG;AACH,MAAa,sBAAsB;IAAnC;QACW,SAAI,GAAG,aAAa,CAAC;IAwChC,CAAC;IAnCC,8DAA8D;IACtD,IAAI;QACV,IAAI,CAAC;YACH,8EAA8E;YAC9E,OAAO,OAAO,CAAC,6BAA6B,CAAC,CAAC;QAChD,CAAC;QAAC,MAAM,CAAC;YACP,MAAM,IAAI,KAAK,CACb,8EAA8E;gBAC5E,2CAA2C,CAC9C,CAAC;QACJ,CAAC;IACH,CAAC;IAED,KAAK,CAAC,IAAI,CAAC,KAAkB;QAC3B,MAAM,OAAO,GAAG,OAAO,CAAC,GAAG,CAAC,sBAAsB,CAAC;QACnD,IAAI,CAAC,OAAO,EAAE,CAAC;YACb,MAAM,IAAI,KAAK,CAAC,mCAAmC,CAAC,CAAC;QACvD,CAAC;QACD,MAAM,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC;QAC5D,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC;YACjB,IAAI,CAAC,MAAM,GAAG,IAAI,iBAAiB,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,GAAG,CAAC,UAAU,EAAE,CAAC,CAAC;QAC1E,CAAC;QACD,MAAM,IAAI,CAAC,MAAM,CAAC,IAAI,CACpB,IAAI,gBAAgB,CAAC;YACnB,OAAO,EAAE;gBACP;oBACE,YAAY,EAAE,OAAO;oBACrB,MAAM,EAAE,oBAAoB;oBAC5B,UAAU,EAAE,KAAK,CAAC,IAAI;oBACtB,MAAM,EAAE,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC;iBAC9B;aACF;SACF,CAAC,CACH,CAAC;IACJ,CAAC;CACF;AAzCD,wDAyCC"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { DeliveryDestination, DOMAIN_EVENT_DESTINATIONS } from './delivery-destination';
|
|
2
|
+
export { DeliveryDestination, DOMAIN_EVENT_DESTINATIONS };
|
|
3
|
+
export { signWebhook, WebhookDestination } from './webhook.destination';
|
|
4
|
+
export { KafkaDestination } from './kafka.destination';
|
|
5
|
+
export { SqsDestination } from './sqs.destination';
|
|
6
|
+
export { EventBridgeDestination } from './eventbridge.destination';
|
|
7
|
+
/**
|
|
8
|
+
* Builds the ACTIVE set of destinations from the runtime `EVENTS_DESTINATION`
|
|
9
|
+
* env var (comma-separated, trimmed). This is the ONLY place delivery is
|
|
10
|
+
* selected — there is no build-time/`.apsorc` delivery config. An empty
|
|
11
|
+
* EVENTS_DESTINATION means nothing is delivered (events stay `pending`).
|
|
12
|
+
*
|
|
13
|
+
* Broker client libraries are loaded lazily inside each adapter, so a service
|
|
14
|
+
* only needs the dependency for the destination(s) it actually activates here.
|
|
15
|
+
*
|
|
16
|
+
* Delivery is at-least-once and, with multiple active destinations, re-sends to
|
|
17
|
+
* ALL of them on retry — consumers MUST dedupe on `event.id`.
|
|
18
|
+
*
|
|
19
|
+
* An unknown EVENTS_DESTINATION value is a startup error.
|
|
20
|
+
*/
|
|
21
|
+
export declare function buildDestinations(): DeliveryDestination[];
|
|
22
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/destinations/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,mBAAmB,EACnB,yBAAyB,EAC1B,MAAM,wBAAwB,CAAC;AAMhC,OAAO,EAAE,mBAAmB,EAAE,yBAAyB,EAAE,CAAC;AAC1D,OAAO,EAAE,WAAW,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AACxE,OAAO,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AACvD,OAAO,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AACnD,OAAO,EAAE,sBAAsB,EAAE,MAAM,2BAA2B,CAAC;AAcnE;;;;;;;;;;;;;GAaG;AACH,wBAAgB,iBAAiB,IAAI,mBAAmB,EAAE,CAiBzD"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.EventBridgeDestination = exports.SqsDestination = exports.KafkaDestination = exports.WebhookDestination = exports.signWebhook = exports.DOMAIN_EVENT_DESTINATIONS = void 0;
|
|
4
|
+
exports.buildDestinations = buildDestinations;
|
|
5
|
+
const delivery_destination_1 = require("./delivery-destination");
|
|
6
|
+
Object.defineProperty(exports, "DOMAIN_EVENT_DESTINATIONS", { enumerable: true, get: function () { return delivery_destination_1.DOMAIN_EVENT_DESTINATIONS; } });
|
|
7
|
+
const webhook_destination_1 = require("./webhook.destination");
|
|
8
|
+
const kafka_destination_1 = require("./kafka.destination");
|
|
9
|
+
const sqs_destination_1 = require("./sqs.destination");
|
|
10
|
+
const eventbridge_destination_1 = require("./eventbridge.destination");
|
|
11
|
+
var webhook_destination_2 = require("./webhook.destination");
|
|
12
|
+
Object.defineProperty(exports, "signWebhook", { enumerable: true, get: function () { return webhook_destination_2.signWebhook; } });
|
|
13
|
+
Object.defineProperty(exports, "WebhookDestination", { enumerable: true, get: function () { return webhook_destination_2.WebhookDestination; } });
|
|
14
|
+
var kafka_destination_2 = require("./kafka.destination");
|
|
15
|
+
Object.defineProperty(exports, "KafkaDestination", { enumerable: true, get: function () { return kafka_destination_2.KafkaDestination; } });
|
|
16
|
+
var sqs_destination_2 = require("./sqs.destination");
|
|
17
|
+
Object.defineProperty(exports, "SqsDestination", { enumerable: true, get: function () { return sqs_destination_2.SqsDestination; } });
|
|
18
|
+
var eventbridge_destination_2 = require("./eventbridge.destination");
|
|
19
|
+
Object.defineProperty(exports, "EventBridgeDestination", { enumerable: true, get: function () { return eventbridge_destination_2.EventBridgeDestination; } });
|
|
20
|
+
/**
|
|
21
|
+
* Factory for every supported {@link DeliveryDestination} adapter, keyed by
|
|
22
|
+
* name. All adapters are always present; broker SDKs are loaded lazily inside
|
|
23
|
+
* the adapter only when it is actually activated.
|
|
24
|
+
*/
|
|
25
|
+
const DESTINATION_FACTORIES = {
|
|
26
|
+
webhook: () => new webhook_destination_1.WebhookDestination(),
|
|
27
|
+
kafka: () => new kafka_destination_1.KafkaDestination(),
|
|
28
|
+
sqs: () => new sqs_destination_1.SqsDestination(),
|
|
29
|
+
eventbridge: () => new eventbridge_destination_1.EventBridgeDestination(),
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* Builds the ACTIVE set of destinations from the runtime `EVENTS_DESTINATION`
|
|
33
|
+
* env var (comma-separated, trimmed). This is the ONLY place delivery is
|
|
34
|
+
* selected — there is no build-time/`.apsorc` delivery config. An empty
|
|
35
|
+
* EVENTS_DESTINATION means nothing is delivered (events stay `pending`).
|
|
36
|
+
*
|
|
37
|
+
* Broker client libraries are loaded lazily inside each adapter, so a service
|
|
38
|
+
* only needs the dependency for the destination(s) it actually activates here.
|
|
39
|
+
*
|
|
40
|
+
* Delivery is at-least-once and, with multiple active destinations, re-sends to
|
|
41
|
+
* ALL of them on retry — consumers MUST dedupe on `event.id`.
|
|
42
|
+
*
|
|
43
|
+
* An unknown EVENTS_DESTINATION value is a startup error.
|
|
44
|
+
*/
|
|
45
|
+
function buildDestinations() {
|
|
46
|
+
const raw = process.env.EVENTS_DESTINATION ?? '';
|
|
47
|
+
const names = raw
|
|
48
|
+
.split(',')
|
|
49
|
+
.map((name) => name.trim())
|
|
50
|
+
.filter(Boolean);
|
|
51
|
+
return names.map((name) => {
|
|
52
|
+
const factory = DESTINATION_FACTORIES[name];
|
|
53
|
+
if (!factory) {
|
|
54
|
+
throw new Error(`EVENTS_DESTINATION '${name}' is not a known destination. ` +
|
|
55
|
+
`Valid values: [${Object.keys(DESTINATION_FACTORIES).join(', ')}]`);
|
|
56
|
+
}
|
|
57
|
+
return factory();
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/destinations/index.ts"],"names":[],"mappings":";;;AAyCA,8CAiBC;AA1DD,iEAGgC;AAMF,0GAP5B,gDAAyB,OAO4B;AALvD,+DAA2D;AAC3D,2DAAuD;AACvD,uDAAmD;AACnD,uEAAmE;AAGnE,6DAAwE;AAA/D,kHAAA,WAAW,OAAA;AAAE,yHAAA,kBAAkB,OAAA;AACxC,yDAAuD;AAA9C,qHAAA,gBAAgB,OAAA;AACzB,qDAAmD;AAA1C,iHAAA,cAAc,OAAA;AACvB,qEAAmE;AAA1D,iIAAA,sBAAsB,OAAA;AAE/B;;;;GAIG;AACH,MAAM,qBAAqB,GAA8C;IACvE,OAAO,EAAE,GAAG,EAAE,CAAC,IAAI,wCAAkB,EAAE;IACvC,KAAK,EAAE,GAAG,EAAE,CAAC,IAAI,oCAAgB,EAAE;IACnC,GAAG,EAAE,GAAG,EAAE,CAAC,IAAI,gCAAc,EAAE;IAC/B,WAAW,EAAE,GAAG,EAAE,CAAC,IAAI,gDAAsB,EAAE;CAChD,CAAC;AAEF;;;;;;;;;;;;;GAaG;AACH,SAAgB,iBAAiB;IAC/B,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,kBAAkB,IAAI,EAAE,CAAC;IACjD,MAAM,KAAK,GAAG,GAAG;SACd,KAAK,CAAC,GAAG,CAAC;SACV,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;SAC1B,MAAM,CAAC,OAAO,CAAC,CAAC;IAEnB,OAAO,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE;QACxB,MAAM,OAAO,GAAG,qBAAqB,CAAC,IAAI,CAAC,CAAC;QAC5C,IAAI,CAAC,OAAO,EAAE,CAAC;YACb,MAAM,IAAI,KAAK,CACb,uBAAuB,IAAI,gCAAgC;gBACzD,kBAAkB,MAAM,CAAC,IAAI,CAAC,qBAAqB,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CACrE,CAAC;QACJ,CAAC;QACD,OAAO,OAAO,EAAE,CAAC;IACnB,CAAC,CAAC,CAAC;AACL,CAAC"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { DomainEvent } from '../domain-event.entity';
|
|
2
|
+
import { DeliveryDestination } from './delivery-destination';
|
|
3
|
+
/**
|
|
4
|
+
* KafkaDestination emits each domain event onto a Kafka topic using the
|
|
5
|
+
* idiomatic `@nestjs/microservices` ClientProxy.
|
|
6
|
+
*
|
|
7
|
+
* `@nestjs/microservices`, `kafkajs` and `rxjs` are loaded LAZILY (require) so
|
|
8
|
+
* they are only needed when this destination is actually activated at runtime
|
|
9
|
+
* (EVENTS_DESTINATION=kafka). Install them in the deploying service:
|
|
10
|
+
* `npm install @nestjs/microservices kafkajs rxjs`.
|
|
11
|
+
*
|
|
12
|
+
* Env:
|
|
13
|
+
* EVENTS_KAFKA_BROKERS — comma-separated broker list (required)
|
|
14
|
+
* EVENTS_KAFKA_TOPIC — topic to emit to (required)
|
|
15
|
+
*/
|
|
16
|
+
export declare class KafkaDestination implements DeliveryDestination {
|
|
17
|
+
readonly name = "kafka";
|
|
18
|
+
private client?;
|
|
19
|
+
private connected;
|
|
20
|
+
private loadDep;
|
|
21
|
+
private getClient;
|
|
22
|
+
send(event: DomainEvent): Promise<void>;
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=kafka.destination.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"kafka.destination.d.ts","sourceRoot":"","sources":["../../src/destinations/kafka.destination.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AACrD,OAAO,EAAE,mBAAmB,EAAE,MAAM,wBAAwB,CAAC;AAE7D;;;;;;;;;;;;GAYG;AACH,qBAAa,gBAAiB,YAAW,mBAAmB;IAC1D,QAAQ,CAAC,IAAI,WAAW;IAGxB,OAAO,CAAC,MAAM,CAAC,CAAM;IACrB,OAAO,CAAC,SAAS,CAAS;IAG1B,OAAO,CAAC,OAAO;IAaf,OAAO,CAAC,SAAS;IAoBX,IAAI,CAAC,KAAK,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC;CAa9C"}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.KafkaDestination = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* KafkaDestination emits each domain event onto a Kafka topic using the
|
|
6
|
+
* idiomatic `@nestjs/microservices` ClientProxy.
|
|
7
|
+
*
|
|
8
|
+
* `@nestjs/microservices`, `kafkajs` and `rxjs` are loaded LAZILY (require) so
|
|
9
|
+
* they are only needed when this destination is actually activated at runtime
|
|
10
|
+
* (EVENTS_DESTINATION=kafka). Install them in the deploying service:
|
|
11
|
+
* `npm install @nestjs/microservices kafkajs rxjs`.
|
|
12
|
+
*
|
|
13
|
+
* Env:
|
|
14
|
+
* EVENTS_KAFKA_BROKERS — comma-separated broker list (required)
|
|
15
|
+
* EVENTS_KAFKA_TOPIC — topic to emit to (required)
|
|
16
|
+
*/
|
|
17
|
+
class KafkaDestination {
|
|
18
|
+
constructor() {
|
|
19
|
+
this.name = 'kafka';
|
|
20
|
+
this.connected = false;
|
|
21
|
+
}
|
|
22
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
23
|
+
loadDep(pkg) {
|
|
24
|
+
try {
|
|
25
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires, global-require
|
|
26
|
+
return require(pkg);
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
throw new Error("EVENTS_DESTINATION=kafka requires '@nestjs/microservices', 'kafkajs' " +
|
|
30
|
+
"and 'rxjs' — run `npm install @nestjs/microservices kafkajs rxjs`");
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
34
|
+
getClient() {
|
|
35
|
+
if (!this.client) {
|
|
36
|
+
const brokers = (process.env.EVENTS_KAFKA_BROKERS ?? '')
|
|
37
|
+
.split(',')
|
|
38
|
+
.map((broker) => broker.trim())
|
|
39
|
+
.filter(Boolean);
|
|
40
|
+
if (brokers.length === 0) {
|
|
41
|
+
throw new Error('EVENTS_KAFKA_BROKERS is not set');
|
|
42
|
+
}
|
|
43
|
+
const { ClientProxyFactory, Transport } = this.loadDep('@nestjs/microservices');
|
|
44
|
+
this.client = ClientProxyFactory.create({
|
|
45
|
+
transport: Transport.KAFKA,
|
|
46
|
+
options: { client: { brokers } },
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
return this.client;
|
|
50
|
+
}
|
|
51
|
+
async send(event) {
|
|
52
|
+
if (!process.env.EVENTS_KAFKA_TOPIC) {
|
|
53
|
+
throw new Error('EVENTS_KAFKA_TOPIC is not set');
|
|
54
|
+
}
|
|
55
|
+
const topic = process.env.EVENTS_KAFKA_TOPIC;
|
|
56
|
+
const { lastValueFrom } = this.loadDep('rxjs');
|
|
57
|
+
const client = this.getClient();
|
|
58
|
+
if (!this.connected) {
|
|
59
|
+
await client.connect();
|
|
60
|
+
this.connected = true;
|
|
61
|
+
}
|
|
62
|
+
await lastValueFrom(client.emit(topic, event));
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
exports.KafkaDestination = KafkaDestination;
|
|
66
|
+
//# sourceMappingURL=kafka.destination.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"kafka.destination.js","sourceRoot":"","sources":["../../src/destinations/kafka.destination.ts"],"names":[],"mappings":";;;AAGA;;;;;;;;;;;;GAYG;AACH,MAAa,gBAAgB;IAA7B;QACW,SAAI,GAAG,OAAO,CAAC;QAIhB,cAAS,GAAG,KAAK,CAAC;IAiD5B,CAAC;IA/CC,8DAA8D;IACtD,OAAO,CAAC,GAAW;QACzB,IAAI,CAAC;YACH,8EAA8E;YAC9E,OAAO,OAAO,CAAC,GAAG,CAAC,CAAC;QACtB,CAAC;QAAC,MAAM,CAAC;YACP,MAAM,IAAI,KAAK,CACb,uEAAuE;gBACrE,mEAAmE,CACtE,CAAC;QACJ,CAAC;IACH,CAAC;IAED,8DAA8D;IACtD,SAAS;QACf,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC;YACjB,MAAM,OAAO,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,oBAAoB,IAAI,EAAE,CAAC;iBACrD,KAAK,CAAC,GAAG,CAAC;iBACV,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC;iBAC9B,MAAM,CAAC,OAAO,CAAC,CAAC;YACnB,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;gBACzB,MAAM,IAAI,KAAK,CAAC,iCAAiC,CAAC,CAAC;YACrD,CAAC;YACD,MAAM,EAAE,kBAAkB,EAAE,SAAS,EAAE,GAAG,IAAI,CAAC,OAAO,CACpD,uBAAuB,CACxB,CAAC;YACF,IAAI,CAAC,MAAM,GAAG,kBAAkB,CAAC,MAAM,CAAC;gBACtC,SAAS,EAAE,SAAS,CAAC,KAAK;gBAC1B,OAAO,EAAE,EAAE,MAAM,EAAE,EAAE,OAAO,EAAE,EAAE;aACjC,CAAC,CAAC;QACL,CAAC;QACD,OAAO,IAAI,CAAC,MAAM,CAAC;IACrB,CAAC;IAED,KAAK,CAAC,IAAI,CAAC,KAAkB;QAC3B,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,kBAAkB,EAAE,CAAC;YACpC,MAAM,IAAI,KAAK,CAAC,+BAA+B,CAAC,CAAC;QACnD,CAAC;QACD,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,CAAC,kBAAkB,CAAC;QAC7C,MAAM,EAAE,aAAa,EAAE,GAAG,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;QAC/C,MAAM,MAAM,GAAG,IAAI,CAAC,SAAS,EAAE,CAAC;QAChC,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,CAAC;YACpB,MAAM,MAAM,CAAC,OAAO,EAAE,CAAC;YACvB,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC;QACxB,CAAC;QACD,MAAM,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC,CAAC;IACjD,CAAC;CACF;AAtDD,4CAsDC"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { DomainEvent } from '../domain-event.entity';
|
|
2
|
+
import { DeliveryDestination } from './delivery-destination';
|
|
3
|
+
/**
|
|
4
|
+
* SqsDestination sends each domain event to an Amazon SQS queue.
|
|
5
|
+
*
|
|
6
|
+
* The AWS SDK is loaded LAZILY (require) so it is only needed when this
|
|
7
|
+
* destination is actually activated at runtime (EVENTS_DESTINATION=sqs). Install
|
|
8
|
+
* it in the deploying service: `npm install @aws-sdk/client-sqs`.
|
|
9
|
+
*
|
|
10
|
+
* Env:
|
|
11
|
+
* AWS_REGION — AWS region (required)
|
|
12
|
+
* EVENTS_SQS_QUEUE_URL — target queue URL (required)
|
|
13
|
+
*/
|
|
14
|
+
export declare class SqsDestination implements DeliveryDestination {
|
|
15
|
+
readonly name = "sqs";
|
|
16
|
+
private client?;
|
|
17
|
+
private load;
|
|
18
|
+
send(event: DomainEvent): Promise<void>;
|
|
19
|
+
}
|
|
20
|
+
//# sourceMappingURL=sqs.destination.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sqs.destination.d.ts","sourceRoot":"","sources":["../../src/destinations/sqs.destination.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AACrD,OAAO,EAAE,mBAAmB,EAAE,MAAM,wBAAwB,CAAC;AAE7D;;;;;;;;;;GAUG;AACH,qBAAa,cAAe,YAAW,mBAAmB;IACxD,QAAQ,CAAC,IAAI,SAAS;IAGtB,OAAO,CAAC,MAAM,CAAC,CAAM;IAGrB,OAAO,CAAC,IAAI;IAYN,IAAI,CAAC,KAAK,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC;CAgB9C"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SqsDestination = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* SqsDestination sends each domain event to an Amazon SQS queue.
|
|
6
|
+
*
|
|
7
|
+
* The AWS SDK is loaded LAZILY (require) so it is only needed when this
|
|
8
|
+
* destination is actually activated at runtime (EVENTS_DESTINATION=sqs). Install
|
|
9
|
+
* it in the deploying service: `npm install @aws-sdk/client-sqs`.
|
|
10
|
+
*
|
|
11
|
+
* Env:
|
|
12
|
+
* AWS_REGION — AWS region (required)
|
|
13
|
+
* EVENTS_SQS_QUEUE_URL — target queue URL (required)
|
|
14
|
+
*/
|
|
15
|
+
class SqsDestination {
|
|
16
|
+
constructor() {
|
|
17
|
+
this.name = 'sqs';
|
|
18
|
+
}
|
|
19
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
20
|
+
load() {
|
|
21
|
+
try {
|
|
22
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires, global-require
|
|
23
|
+
return require('@aws-sdk/client-sqs');
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
throw new Error("EVENTS_DESTINATION=sqs requires '@aws-sdk/client-sqs' — run " +
|
|
27
|
+
'`npm install @aws-sdk/client-sqs`');
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
async send(event) {
|
|
31
|
+
const queueUrl = process.env.EVENTS_SQS_QUEUE_URL;
|
|
32
|
+
if (!queueUrl) {
|
|
33
|
+
throw new Error('EVENTS_SQS_QUEUE_URL is not set');
|
|
34
|
+
}
|
|
35
|
+
const { SQSClient, SendMessageCommand } = this.load();
|
|
36
|
+
if (!this.client) {
|
|
37
|
+
this.client = new SQSClient({ region: process.env.AWS_REGION });
|
|
38
|
+
}
|
|
39
|
+
await this.client.send(new SendMessageCommand({
|
|
40
|
+
QueueUrl: queueUrl,
|
|
41
|
+
MessageBody: JSON.stringify(event),
|
|
42
|
+
}));
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
exports.SqsDestination = SqsDestination;
|
|
46
|
+
//# sourceMappingURL=sqs.destination.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sqs.destination.js","sourceRoot":"","sources":["../../src/destinations/sqs.destination.ts"],"names":[],"mappings":";;;AAGA;;;;;;;;;;GAUG;AACH,MAAa,cAAc;IAA3B;QACW,SAAI,GAAG,KAAK,CAAC;IAkCxB,CAAC;IA7BC,8DAA8D;IACtD,IAAI;QACV,IAAI,CAAC;YACH,8EAA8E;YAC9E,OAAO,OAAO,CAAC,qBAAqB,CAAC,CAAC;QACxC,CAAC;QAAC,MAAM,CAAC;YACP,MAAM,IAAI,KAAK,CACb,8DAA8D;gBAC5D,mCAAmC,CACtC,CAAC;QACJ,CAAC;IACH,CAAC;IAED,KAAK,CAAC,IAAI,CAAC,KAAkB;QAC3B,MAAM,QAAQ,GAAG,OAAO,CAAC,GAAG,CAAC,oBAAoB,CAAC;QAClD,IAAI,CAAC,QAAQ,EAAE,CAAC;YACd,MAAM,IAAI,KAAK,CAAC,iCAAiC,CAAC,CAAC;QACrD,CAAC;QACD,MAAM,EAAE,SAAS,EAAE,kBAAkB,EAAE,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC;QACtD,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC;YACjB,IAAI,CAAC,MAAM,GAAG,IAAI,SAAS,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,GAAG,CAAC,UAAU,EAAE,CAAC,CAAC;QAClE,CAAC;QACD,MAAM,IAAI,CAAC,MAAM,CAAC,IAAI,CACpB,IAAI,kBAAkB,CAAC;YACrB,QAAQ,EAAE,QAAQ;YAClB,WAAW,EAAE,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC;SACnC,CAAC,CACH,CAAC;IACJ,CAAC;CACF;AAnCD,wCAmCC"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { DomainEvent } from '../domain-event.entity';
|
|
2
|
+
import { DeliveryDestination } from './delivery-destination';
|
|
3
|
+
/**
|
|
4
|
+
* Computes a Standard Webhooks (https://www.standardwebhooks.com/) signature.
|
|
5
|
+
*
|
|
6
|
+
* Pure & deterministic: given the same id, timestamp, body and secret it always
|
|
7
|
+
* returns the same `v1,<base64>` signature header value. Extracted so it can be
|
|
8
|
+
* unit-tested in isolation from the HTTP transport.
|
|
9
|
+
*
|
|
10
|
+
* @param id The webhook message id (here: the DomainEvent id).
|
|
11
|
+
* @param timestamp Unix timestamp in SECONDS.
|
|
12
|
+
* @param body The exact request body string that will be POSTed.
|
|
13
|
+
* @param secret The signing secret, in `whsec_<base64>` form.
|
|
14
|
+
*/
|
|
15
|
+
export declare function signWebhook(id: string, timestamp: number, body: string, secret: string): string;
|
|
16
|
+
/**
|
|
17
|
+
* WebhookDestination POSTs each domain event to a SINGLE configured URL,
|
|
18
|
+
* HMAC-signed in the Standard Webhooks format. It is deliberately not a
|
|
19
|
+
* registry: fan-out to multiple subscribers is an application concern, not a
|
|
20
|
+
* library one. Reliability comes from the relay's retry loop — this adapter just
|
|
21
|
+
* throws on any non-2xx response.
|
|
22
|
+
*
|
|
23
|
+
* Uses native `fetch` and node `crypto`, so a webhook-only deployment needs zero
|
|
24
|
+
* extra dependencies.
|
|
25
|
+
*
|
|
26
|
+
* Env:
|
|
27
|
+
* EVENTS_WEBHOOK_URL — the single sink URL (required)
|
|
28
|
+
* EVENTS_WEBHOOK_SECRET — signing secret as `whsec_<base64>` (required)
|
|
29
|
+
*/
|
|
30
|
+
export declare class WebhookDestination implements DeliveryDestination {
|
|
31
|
+
readonly name = "webhook";
|
|
32
|
+
send(event: DomainEvent): Promise<void>;
|
|
33
|
+
}
|
|
34
|
+
//# sourceMappingURL=webhook.destination.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"webhook.destination.d.ts","sourceRoot":"","sources":["../../src/destinations/webhook.destination.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AACrD,OAAO,EAAE,mBAAmB,EAAE,MAAM,wBAAwB,CAAC;AAE7D;;;;;;;;;;;GAWG;AACH,wBAAgB,WAAW,CACzB,EAAE,EAAE,MAAM,EACV,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,MAAM,GACb,MAAM,CAOR;AAED;;;;;;;;;;;;;GAaG;AACH,qBAAa,kBAAmB,YAAW,mBAAmB;IAC5D,QAAQ,CAAC,IAAI,aAAa;IAEpB,IAAI,CAAC,KAAK,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC;CAgC9C"}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.WebhookDestination = void 0;
|
|
4
|
+
exports.signWebhook = signWebhook;
|
|
5
|
+
const crypto_1 = require("crypto");
|
|
6
|
+
/**
|
|
7
|
+
* Computes a Standard Webhooks (https://www.standardwebhooks.com/) signature.
|
|
8
|
+
*
|
|
9
|
+
* Pure & deterministic: given the same id, timestamp, body and secret it always
|
|
10
|
+
* returns the same `v1,<base64>` signature header value. Extracted so it can be
|
|
11
|
+
* unit-tested in isolation from the HTTP transport.
|
|
12
|
+
*
|
|
13
|
+
* @param id The webhook message id (here: the DomainEvent id).
|
|
14
|
+
* @param timestamp Unix timestamp in SECONDS.
|
|
15
|
+
* @param body The exact request body string that will be POSTed.
|
|
16
|
+
* @param secret The signing secret, in `whsec_<base64>` form.
|
|
17
|
+
*/
|
|
18
|
+
function signWebhook(id, timestamp, body, secret) {
|
|
19
|
+
const signedContent = `${id}.${timestamp}.${body}`;
|
|
20
|
+
const secretBytes = Buffer.from(secret.replace(/^whsec_/, ''), 'base64');
|
|
21
|
+
const signature = (0, crypto_1.createHmac)('sha256', secretBytes)
|
|
22
|
+
.update(signedContent)
|
|
23
|
+
.digest('base64');
|
|
24
|
+
return `v1,${signature}`;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* WebhookDestination POSTs each domain event to a SINGLE configured URL,
|
|
28
|
+
* HMAC-signed in the Standard Webhooks format. It is deliberately not a
|
|
29
|
+
* registry: fan-out to multiple subscribers is an application concern, not a
|
|
30
|
+
* library one. Reliability comes from the relay's retry loop — this adapter just
|
|
31
|
+
* throws on any non-2xx response.
|
|
32
|
+
*
|
|
33
|
+
* Uses native `fetch` and node `crypto`, so a webhook-only deployment needs zero
|
|
34
|
+
* extra dependencies.
|
|
35
|
+
*
|
|
36
|
+
* Env:
|
|
37
|
+
* EVENTS_WEBHOOK_URL — the single sink URL (required)
|
|
38
|
+
* EVENTS_WEBHOOK_SECRET — signing secret as `whsec_<base64>` (required)
|
|
39
|
+
*/
|
|
40
|
+
class WebhookDestination {
|
|
41
|
+
constructor() {
|
|
42
|
+
this.name = 'webhook';
|
|
43
|
+
}
|
|
44
|
+
async send(event) {
|
|
45
|
+
const url = process.env.EVENTS_WEBHOOK_URL;
|
|
46
|
+
const secret = process.env.EVENTS_WEBHOOK_SECRET;
|
|
47
|
+
if (!url) {
|
|
48
|
+
throw new Error('EVENTS_WEBHOOK_URL is not set');
|
|
49
|
+
}
|
|
50
|
+
if (!secret) {
|
|
51
|
+
throw new Error('EVENTS_WEBHOOK_SECRET is not set');
|
|
52
|
+
}
|
|
53
|
+
const id = event.id;
|
|
54
|
+
const timestamp = Math.floor(Date.now() / 1000);
|
|
55
|
+
const body = JSON.stringify(event);
|
|
56
|
+
const signature = signWebhook(id, timestamp, body, secret);
|
|
57
|
+
const response = await fetch(url, {
|
|
58
|
+
method: 'POST',
|
|
59
|
+
headers: {
|
|
60
|
+
'content-type': 'application/json',
|
|
61
|
+
'webhook-id': id,
|
|
62
|
+
'webhook-timestamp': String(timestamp),
|
|
63
|
+
'webhook-signature': signature,
|
|
64
|
+
},
|
|
65
|
+
body,
|
|
66
|
+
});
|
|
67
|
+
if (!response.ok) {
|
|
68
|
+
throw new Error(`Webhook delivery to ${url} failed with status ${response.status}`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
exports.WebhookDestination = WebhookDestination;
|
|
73
|
+
//# sourceMappingURL=webhook.destination.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"webhook.destination.js","sourceRoot":"","sources":["../../src/destinations/webhook.destination.ts"],"names":[],"mappings":";;;AAgBA,kCAYC;AA5BD,mCAAoC;AAIpC;;;;;;;;;;;GAWG;AACH,SAAgB,WAAW,CACzB,EAAU,EACV,SAAiB,EACjB,IAAY,EACZ,MAAc;IAEd,MAAM,aAAa,GAAG,GAAG,EAAE,IAAI,SAAS,IAAI,IAAI,EAAE,CAAC;IACnD,MAAM,WAAW,GAAG,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,SAAS,EAAE,EAAE,CAAC,EAAE,QAAQ,CAAC,CAAC;IACzE,MAAM,SAAS,GAAG,IAAA,mBAAU,EAAC,QAAQ,EAAE,WAAW,CAAC;SAChD,MAAM,CAAC,aAAa,CAAC;SACrB,MAAM,CAAC,QAAQ,CAAC,CAAC;IACpB,OAAO,MAAM,SAAS,EAAE,CAAC;AAC3B,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAa,kBAAkB;IAA/B;QACW,SAAI,GAAG,SAAS,CAAC;IAkC5B,CAAC;IAhCC,KAAK,CAAC,IAAI,CAAC,KAAkB;QAC3B,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,kBAAkB,CAAC;QAC3C,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,qBAAqB,CAAC;QACjD,IAAI,CAAC,GAAG,EAAE,CAAC;YACT,MAAM,IAAI,KAAK,CAAC,+BAA+B,CAAC,CAAC;QACnD,CAAC;QACD,IAAI,CAAC,MAAM,EAAE,CAAC;YACZ,MAAM,IAAI,KAAK,CAAC,kCAAkC,CAAC,CAAC;QACtD,CAAC;QAED,MAAM,EAAE,GAAG,KAAK,CAAC,EAAE,CAAC;QACpB,MAAM,SAAS,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,CAAC;QAChD,MAAM,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;QACnC,MAAM,SAAS,GAAG,WAAW,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,CAAC,CAAC;QAE3D,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,GAAG,EAAE;YAChC,MAAM,EAAE,MAAM;YACd,OAAO,EAAE;gBACP,cAAc,EAAE,kBAAkB;gBAClC,YAAY,EAAE,EAAE;gBAChB,mBAAmB,EAAE,MAAM,CAAC,SAAS,CAAC;gBACtC,mBAAmB,EAAE,SAAS;aAC/B;YACD,IAAI;SACL,CAAC,CAAC;QAEH,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;YACjB,MAAM,IAAI,KAAK,CACb,uBAAuB,GAAG,uBAAuB,QAAQ,CAAC,MAAM,EAAE,CACnE,CAAC;QACJ,CAAC;IACH,CAAC;CACF;AAnCD,gDAmCC"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DomainEvent is a durable, append-only log of domain state changes.
|
|
3
|
+
*
|
|
4
|
+
* Rows are written in the SAME database transaction as the state change that
|
|
5
|
+
* produced them (see {@link DomainEventSubscriber}), which is what makes
|
|
6
|
+
* delivery durable. This implements the standard "transactional outbox"
|
|
7
|
+
* pattern, but is surfaced with generic domain-event naming — no public
|
|
8
|
+
* artifact is named "outbox".
|
|
9
|
+
*
|
|
10
|
+
* A separate {@link DomainEventRelay} polls pending rows and delivers them.
|
|
11
|
+
* Because the stable `id` is preserved across delivery attempts, consumers can
|
|
12
|
+
* dedupe on it for at-least-once delivery semantics.
|
|
13
|
+
*/
|
|
14
|
+
export declare class DomainEvent {
|
|
15
|
+
id: string;
|
|
16
|
+
/** Event type, e.g. "product.created". */
|
|
17
|
+
type: string;
|
|
18
|
+
/** Serialized event payload. */
|
|
19
|
+
payload: Record<string, unknown>;
|
|
20
|
+
/** Delivery lifecycle: pending -> published, or failed after max attempts. */
|
|
21
|
+
status: 'pending' | 'published' | 'failed';
|
|
22
|
+
/** Number of delivery attempts made by the relay. */
|
|
23
|
+
attempts: number;
|
|
24
|
+
created_at: Date;
|
|
25
|
+
/** When the event was successfully published; null until then. */
|
|
26
|
+
publishedAt: Date | null;
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=domain-event.entity.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"domain-event.entity.d.ts","sourceRoot":"","sources":["../src/domain-event.entity.ts"],"names":[],"mappings":"AAQA;;;;;;;;;;;;GAYG;AACH,qBAEa,WAAW;IAEtB,EAAE,EAAG,MAAM,CAAC;IAEZ,0CAA0C;IAE1C,IAAI,EAAG,MAAM,CAAC;IAEd,gCAAgC;IAEhC,OAAO,EAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAElC,8EAA8E;IAE9E,MAAM,EAAG,SAAS,GAAG,WAAW,GAAG,QAAQ,CAAC;IAE5C,qDAAqD;IAErD,QAAQ,EAAG,MAAM,CAAC;IAGlB,UAAU,EAAG,IAAI,CAAC;IAElB,kEAAkE;IAElE,WAAW,EAAG,IAAI,GAAG,IAAI,CAAC;CAC3B"}
|