@hasna/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/LICENSE +17 -0
- package/README.md +240 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +811 -0
- package/dist/commander.d.ts +14 -0
- package/dist/commander.js +691 -0
- package/dist/filter.d.ts +4 -0
- package/dist/filter.js +48 -0
- package/dist/index.d.ts +37 -0
- package/dist/index.js +550 -0
- package/dist/signing.d.ts +9 -0
- package/dist/signing.js +39 -0
- package/dist/storage.d.ts +44 -0
- package/dist/storage.js +120 -0
- package/dist/transports.d.ts +12 -0
- package/dist/transports.js +191 -0
- package/dist/types.d.ts +118 -0
- package/dist/types.js +1 -0
- package/package.json +81 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { ChannelConfig, DeliveryResult, EventEnvelope, StoredEventsData } from "./types.js";
|
|
2
|
+
export declare const HASNA_EVENTS_DIR_ENV = "HASNA_EVENTS_DIR";
|
|
3
|
+
export declare const HASNA_EVENTS_HOME_ENV = "HASNA_EVENTS_HOME";
|
|
4
|
+
export declare function getEventsDataDir(override?: string): string;
|
|
5
|
+
export interface EventsStore {
|
|
6
|
+
dataDir: string;
|
|
7
|
+
init(): Promise<void>;
|
|
8
|
+
addChannel(channel: ChannelConfig): Promise<ChannelConfig>;
|
|
9
|
+
listChannels(): Promise<ChannelConfig[]>;
|
|
10
|
+
getChannel(id: string): Promise<ChannelConfig | undefined>;
|
|
11
|
+
removeChannel(id: string): Promise<boolean>;
|
|
12
|
+
appendEvent(event: EventEnvelope): Promise<EventEnvelope>;
|
|
13
|
+
listEvents(): Promise<EventEnvelope[]>;
|
|
14
|
+
findEventByIdentity(identity: {
|
|
15
|
+
id?: string;
|
|
16
|
+
dedupeKey?: string;
|
|
17
|
+
}): Promise<EventEnvelope | undefined>;
|
|
18
|
+
appendDelivery(result: DeliveryResult): Promise<DeliveryResult>;
|
|
19
|
+
listDeliveries(): Promise<DeliveryResult[]>;
|
|
20
|
+
}
|
|
21
|
+
export declare class JsonEventsStore implements EventsStore {
|
|
22
|
+
dataDir: string;
|
|
23
|
+
private channelsPath;
|
|
24
|
+
private eventsPath;
|
|
25
|
+
private deliveriesPath;
|
|
26
|
+
constructor(dataDir?: string);
|
|
27
|
+
init(): Promise<void>;
|
|
28
|
+
addChannel(channel: ChannelConfig): Promise<ChannelConfig>;
|
|
29
|
+
listChannels(): Promise<ChannelConfig[]>;
|
|
30
|
+
getChannel(id: string): Promise<ChannelConfig | undefined>;
|
|
31
|
+
removeChannel(id: string): Promise<boolean>;
|
|
32
|
+
appendEvent(event: EventEnvelope): Promise<EventEnvelope>;
|
|
33
|
+
listEvents(): Promise<EventEnvelope[]>;
|
|
34
|
+
findEventByIdentity(identity: {
|
|
35
|
+
id?: string;
|
|
36
|
+
dedupeKey?: string;
|
|
37
|
+
}): Promise<EventEnvelope | undefined>;
|
|
38
|
+
appendDelivery(result: DeliveryResult): Promise<DeliveryResult>;
|
|
39
|
+
listDeliveries(): Promise<DeliveryResult[]>;
|
|
40
|
+
exportData(): Promise<StoredEventsData>;
|
|
41
|
+
private ensureArrayFile;
|
|
42
|
+
private readJson;
|
|
43
|
+
private writeJson;
|
|
44
|
+
}
|
package/dist/storage.js
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// src/storage.ts
|
|
3
|
+
import { mkdir, readFile, rename, writeFile } from "fs/promises";
|
|
4
|
+
import { existsSync } from "fs";
|
|
5
|
+
import { homedir } from "os";
|
|
6
|
+
import { join } from "path";
|
|
7
|
+
var HASNA_EVENTS_DIR_ENV = "HASNA_EVENTS_DIR";
|
|
8
|
+
var HASNA_EVENTS_HOME_ENV = "HASNA_EVENTS_HOME";
|
|
9
|
+
function getEventsDataDir(override) {
|
|
10
|
+
return override || process.env[HASNA_EVENTS_DIR_ENV] || process.env[HASNA_EVENTS_HOME_ENV] || join(homedir(), ".hasna", "events");
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
class JsonEventsStore {
|
|
14
|
+
dataDir;
|
|
15
|
+
channelsPath;
|
|
16
|
+
eventsPath;
|
|
17
|
+
deliveriesPath;
|
|
18
|
+
constructor(dataDir = getEventsDataDir()) {
|
|
19
|
+
this.dataDir = dataDir;
|
|
20
|
+
this.channelsPath = join(dataDir, "channels.json");
|
|
21
|
+
this.eventsPath = join(dataDir, "events.json");
|
|
22
|
+
this.deliveriesPath = join(dataDir, "deliveries.json");
|
|
23
|
+
}
|
|
24
|
+
async init() {
|
|
25
|
+
await mkdir(this.dataDir, { recursive: true });
|
|
26
|
+
await this.ensureArrayFile(this.channelsPath);
|
|
27
|
+
await this.ensureArrayFile(this.eventsPath);
|
|
28
|
+
await this.ensureArrayFile(this.deliveriesPath);
|
|
29
|
+
}
|
|
30
|
+
async addChannel(channel) {
|
|
31
|
+
await this.init();
|
|
32
|
+
const channels = await this.readJson(this.channelsPath, []);
|
|
33
|
+
const index = channels.findIndex((item) => item.id === channel.id);
|
|
34
|
+
if (index >= 0) {
|
|
35
|
+
channels[index] = { ...channel, createdAt: channels[index].createdAt, updatedAt: new Date().toISOString() };
|
|
36
|
+
} else {
|
|
37
|
+
channels.push(channel);
|
|
38
|
+
}
|
|
39
|
+
await this.writeJson(this.channelsPath, channels);
|
|
40
|
+
return index >= 0 ? channels[index] : channel;
|
|
41
|
+
}
|
|
42
|
+
async listChannels() {
|
|
43
|
+
await this.init();
|
|
44
|
+
return this.readJson(this.channelsPath, []);
|
|
45
|
+
}
|
|
46
|
+
async getChannel(id) {
|
|
47
|
+
const channels = await this.listChannels();
|
|
48
|
+
return channels.find((channel) => channel.id === id);
|
|
49
|
+
}
|
|
50
|
+
async removeChannel(id) {
|
|
51
|
+
await this.init();
|
|
52
|
+
const channels = await this.readJson(this.channelsPath, []);
|
|
53
|
+
const next = channels.filter((channel) => channel.id !== id);
|
|
54
|
+
await this.writeJson(this.channelsPath, next);
|
|
55
|
+
return next.length !== channels.length;
|
|
56
|
+
}
|
|
57
|
+
async appendEvent(event) {
|
|
58
|
+
await this.init();
|
|
59
|
+
const events = await this.readJson(this.eventsPath, []);
|
|
60
|
+
events.push(event);
|
|
61
|
+
await this.writeJson(this.eventsPath, events);
|
|
62
|
+
return event;
|
|
63
|
+
}
|
|
64
|
+
async listEvents() {
|
|
65
|
+
await this.init();
|
|
66
|
+
return this.readJson(this.eventsPath, []);
|
|
67
|
+
}
|
|
68
|
+
async findEventByIdentity(identity) {
|
|
69
|
+
const events = await this.listEvents();
|
|
70
|
+
return events.find((event) => identity.id !== undefined && event.id === identity.id || identity.dedupeKey !== undefined && event.dedupeKey === identity.dedupeKey);
|
|
71
|
+
}
|
|
72
|
+
async appendDelivery(result) {
|
|
73
|
+
await this.init();
|
|
74
|
+
const deliveries = await this.readJson(this.deliveriesPath, []);
|
|
75
|
+
deliveries.push(result);
|
|
76
|
+
await this.writeJson(this.deliveriesPath, deliveries);
|
|
77
|
+
return result;
|
|
78
|
+
}
|
|
79
|
+
async listDeliveries() {
|
|
80
|
+
await this.init();
|
|
81
|
+
return this.readJson(this.deliveriesPath, []);
|
|
82
|
+
}
|
|
83
|
+
async exportData() {
|
|
84
|
+
return {
|
|
85
|
+
channels: await this.listChannels(),
|
|
86
|
+
events: await this.listEvents(),
|
|
87
|
+
deliveries: await this.listDeliveries()
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
async ensureArrayFile(path) {
|
|
91
|
+
if (!existsSync(path)) {
|
|
92
|
+
await writeFile(path, `[]
|
|
93
|
+
`, "utf-8");
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
async readJson(path, fallback) {
|
|
97
|
+
try {
|
|
98
|
+
const raw = await readFile(path, "utf-8");
|
|
99
|
+
if (!raw.trim())
|
|
100
|
+
return fallback;
|
|
101
|
+
return JSON.parse(raw);
|
|
102
|
+
} catch (error) {
|
|
103
|
+
if (error.code === "ENOENT")
|
|
104
|
+
return fallback;
|
|
105
|
+
throw error;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
async writeJson(path, value) {
|
|
109
|
+
const tempPath = `${path}.${process.pid}.${Date.now()}.tmp`;
|
|
110
|
+
await writeFile(tempPath, `${JSON.stringify(value, null, 2)}
|
|
111
|
+
`, "utf-8");
|
|
112
|
+
await rename(tempPath, path);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
export {
|
|
116
|
+
getEventsDataDir,
|
|
117
|
+
JsonEventsStore,
|
|
118
|
+
HASNA_EVENTS_HOME_ENV,
|
|
119
|
+
HASNA_EVENTS_DIR_ENV
|
|
120
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ChannelConfig, DeliveryAttempt, DeliveryResult, EventEnvelope } from "./types.js";
|
|
2
|
+
export interface TransportDispatchOptions {
|
|
3
|
+
fetchImpl?: (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
|
|
4
|
+
}
|
|
5
|
+
export declare function buildWebhookRequest(event: EventEnvelope, channel: ChannelConfig): {
|
|
6
|
+
body: string;
|
|
7
|
+
headers: Record<string, string>;
|
|
8
|
+
};
|
|
9
|
+
export declare function dispatchWebhook(event: EventEnvelope, channel: ChannelConfig, options?: TransportDispatchOptions): Promise<DeliveryAttempt>;
|
|
10
|
+
export declare function dispatchCommand(event: EventEnvelope, channel: ChannelConfig): Promise<DeliveryAttempt>;
|
|
11
|
+
export declare function dispatchChannel(event: EventEnvelope, channel: ChannelConfig, options?: TransportDispatchOptions): Promise<DeliveryAttempt>;
|
|
12
|
+
export declare function createDeliveryResult(event: EventEnvelope, channel: ChannelConfig, attempts: DeliveryAttempt[]): DeliveryResult;
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// src/signing.ts
|
|
3
|
+
import { createHmac, timingSafeEqual } from "crypto";
|
|
4
|
+
function buildSignatureBase(timestamp, body) {
|
|
5
|
+
return `${timestamp}.${body}`;
|
|
6
|
+
}
|
|
7
|
+
function signPayload(secret, timestamp, body) {
|
|
8
|
+
const digest = createHmac("sha256", secret).update(buildSignatureBase(timestamp, body)).digest("hex");
|
|
9
|
+
return `sha256=${digest}`;
|
|
10
|
+
}
|
|
11
|
+
function verifyPayloadSignature(secret, timestamp, body, signature) {
|
|
12
|
+
const expected = signPayload(secret, timestamp, body);
|
|
13
|
+
const actual = signature.trim();
|
|
14
|
+
const expectedBuffer = Buffer.from(expected);
|
|
15
|
+
const actualBuffer = Buffer.from(actual);
|
|
16
|
+
if (expectedBuffer.length !== actualBuffer.length)
|
|
17
|
+
return false;
|
|
18
|
+
return timingSafeEqual(expectedBuffer, actualBuffer);
|
|
19
|
+
}
|
|
20
|
+
function isTimestampWithinTolerance(timestamp, toleranceMs, now = Date.now()) {
|
|
21
|
+
const parsed = Date.parse(timestamp);
|
|
22
|
+
if (!Number.isFinite(parsed))
|
|
23
|
+
return false;
|
|
24
|
+
const reference = now instanceof Date ? now.getTime() : now;
|
|
25
|
+
return Math.abs(reference - parsed) <= toleranceMs;
|
|
26
|
+
}
|
|
27
|
+
function verifyWebhookSignature(secret, timestamp, body, signature, options = {}) {
|
|
28
|
+
if (options.toleranceMs !== undefined && !isTimestampWithinTolerance(timestamp, options.toleranceMs, options.now)) {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
return verifyPayloadSignature(secret, timestamp, body, signature);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// src/transports.ts
|
|
35
|
+
import { randomUUID } from "crypto";
|
|
36
|
+
import { spawn } from "child_process";
|
|
37
|
+
function now() {
|
|
38
|
+
return new Date().toISOString();
|
|
39
|
+
}
|
|
40
|
+
function truncate(value, max = 4096) {
|
|
41
|
+
return value.length > max ? `${value.slice(0, max)}...` : value;
|
|
42
|
+
}
|
|
43
|
+
function buildWebhookRequest(event, channel) {
|
|
44
|
+
if (!channel.webhook)
|
|
45
|
+
throw new Error(`Channel ${channel.id} has no webhook config`);
|
|
46
|
+
const body = JSON.stringify(event);
|
|
47
|
+
const timestamp = event.time;
|
|
48
|
+
const headers = {
|
|
49
|
+
"Content-Type": "application/json",
|
|
50
|
+
"User-Agent": "@hasna/events",
|
|
51
|
+
"X-Hasna-Event-Id": event.id,
|
|
52
|
+
"X-Hasna-Event-Type": event.type,
|
|
53
|
+
"X-Hasna-Timestamp": timestamp,
|
|
54
|
+
...channel.webhook.headers
|
|
55
|
+
};
|
|
56
|
+
if (channel.webhook.secret) {
|
|
57
|
+
headers["X-Hasna-Signature"] = signPayload(channel.webhook.secret, timestamp, body);
|
|
58
|
+
}
|
|
59
|
+
return { body, headers };
|
|
60
|
+
}
|
|
61
|
+
async function dispatchWebhook(event, channel, options = {}) {
|
|
62
|
+
if (!channel.webhook)
|
|
63
|
+
throw new Error(`Channel ${channel.id} has no webhook config`);
|
|
64
|
+
const startedAt = now();
|
|
65
|
+
const { body, headers } = buildWebhookRequest(event, channel);
|
|
66
|
+
const controller = new AbortController;
|
|
67
|
+
const timeout = setTimeout(() => controller.abort(), channel.webhook.timeoutMs ?? 15000);
|
|
68
|
+
try {
|
|
69
|
+
const response = await (options.fetchImpl ?? fetch)(channel.webhook.url, {
|
|
70
|
+
method: "POST",
|
|
71
|
+
headers,
|
|
72
|
+
body,
|
|
73
|
+
signal: controller.signal
|
|
74
|
+
});
|
|
75
|
+
const responseBody = truncate(await response.text());
|
|
76
|
+
return {
|
|
77
|
+
attempt: 1,
|
|
78
|
+
status: response.ok ? "success" : "failed",
|
|
79
|
+
startedAt,
|
|
80
|
+
completedAt: now(),
|
|
81
|
+
responseStatus: response.status,
|
|
82
|
+
responseBody,
|
|
83
|
+
error: response.ok ? undefined : `Webhook returned HTTP ${response.status}`
|
|
84
|
+
};
|
|
85
|
+
} catch (error) {
|
|
86
|
+
return {
|
|
87
|
+
attempt: 1,
|
|
88
|
+
status: "failed",
|
|
89
|
+
startedAt,
|
|
90
|
+
completedAt: now(),
|
|
91
|
+
error: error instanceof Error ? error.message : String(error)
|
|
92
|
+
};
|
|
93
|
+
} finally {
|
|
94
|
+
clearTimeout(timeout);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
async function dispatchCommand(event, channel) {
|
|
98
|
+
if (!channel.command)
|
|
99
|
+
throw new Error(`Channel ${channel.id} has no command config`);
|
|
100
|
+
const startedAt = now();
|
|
101
|
+
const eventJson = JSON.stringify(event);
|
|
102
|
+
const env = {
|
|
103
|
+
...process.env,
|
|
104
|
+
...channel.command.env,
|
|
105
|
+
HASNA_CHANNEL_ID: channel.id,
|
|
106
|
+
HASNA_EVENT_ID: event.id,
|
|
107
|
+
HASNA_EVENT_TYPE: event.type,
|
|
108
|
+
HASNA_EVENT_SOURCE: event.source,
|
|
109
|
+
HASNA_EVENT_SUBJECT: event.subject ?? "",
|
|
110
|
+
HASNA_EVENT_SEVERITY: event.severity,
|
|
111
|
+
HASNA_EVENT_TIME: event.time,
|
|
112
|
+
HASNA_EVENT_DEDUPE_KEY: event.dedupeKey ?? "",
|
|
113
|
+
HASNA_EVENT_SCHEMA_VERSION: event.schemaVersion,
|
|
114
|
+
HASNA_EVENT_JSON: eventJson
|
|
115
|
+
};
|
|
116
|
+
return new Promise((resolve) => {
|
|
117
|
+
const child = spawn(channel.command.command, channel.command.args ?? [], {
|
|
118
|
+
cwd: channel.command.cwd,
|
|
119
|
+
env,
|
|
120
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
121
|
+
});
|
|
122
|
+
let stdout = "";
|
|
123
|
+
let stderr = "";
|
|
124
|
+
const timeout = setTimeout(() => child.kill("SIGTERM"), channel.command.timeoutMs ?? 15000);
|
|
125
|
+
child.stdin.end(eventJson);
|
|
126
|
+
child.stdout.on("data", (chunk) => {
|
|
127
|
+
stdout += chunk.toString();
|
|
128
|
+
});
|
|
129
|
+
child.stderr.on("data", (chunk) => {
|
|
130
|
+
stderr += chunk.toString();
|
|
131
|
+
});
|
|
132
|
+
child.on("error", (error) => {
|
|
133
|
+
clearTimeout(timeout);
|
|
134
|
+
resolve({
|
|
135
|
+
attempt: 1,
|
|
136
|
+
status: "failed",
|
|
137
|
+
startedAt,
|
|
138
|
+
completedAt: now(),
|
|
139
|
+
stdout: truncate(stdout),
|
|
140
|
+
stderr: truncate(stderr),
|
|
141
|
+
error: error.message
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
child.on("close", (code, signal) => {
|
|
145
|
+
clearTimeout(timeout);
|
|
146
|
+
const success = code === 0;
|
|
147
|
+
resolve({
|
|
148
|
+
attempt: 1,
|
|
149
|
+
status: success ? "success" : "failed",
|
|
150
|
+
startedAt,
|
|
151
|
+
completedAt: now(),
|
|
152
|
+
stdout: truncate(stdout),
|
|
153
|
+
stderr: truncate(stderr),
|
|
154
|
+
error: success ? undefined : `Command exited with ${signal ? `signal ${signal}` : `code ${code}`}`
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
async function dispatchChannel(event, channel, options = {}) {
|
|
160
|
+
if (channel.transport === "webhook")
|
|
161
|
+
return dispatchWebhook(event, channel, options);
|
|
162
|
+
if (channel.transport === "command")
|
|
163
|
+
return dispatchCommand(event, channel);
|
|
164
|
+
return {
|
|
165
|
+
attempt: 1,
|
|
166
|
+
status: "skipped",
|
|
167
|
+
startedAt: now(),
|
|
168
|
+
completedAt: now(),
|
|
169
|
+
error: `Unsupported transport: ${channel.transport}`
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
function createDeliveryResult(event, channel, attempts) {
|
|
173
|
+
const status = attempts.some((attempt) => attempt.status === "success") ? "success" : attempts.every((attempt) => attempt.status === "skipped") ? "skipped" : "failed";
|
|
174
|
+
return {
|
|
175
|
+
id: randomUUID(),
|
|
176
|
+
eventId: event.id,
|
|
177
|
+
channelId: channel.id,
|
|
178
|
+
transport: channel.transport,
|
|
179
|
+
status,
|
|
180
|
+
attempts,
|
|
181
|
+
createdAt: attempts[0]?.startedAt ?? now(),
|
|
182
|
+
completedAt: attempts.at(-1)?.completedAt ?? now()
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
export {
|
|
186
|
+
dispatchWebhook,
|
|
187
|
+
dispatchCommand,
|
|
188
|
+
dispatchChannel,
|
|
189
|
+
createDeliveryResult,
|
|
190
|
+
buildWebhookRequest
|
|
191
|
+
};
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
export type EventSeverity = "debug" | "info" | "notice" | "warning" | "error" | "critical";
|
|
2
|
+
export type EventData = Record<string, unknown>;
|
|
3
|
+
export interface EventEnvelope<TData extends EventData = EventData> {
|
|
4
|
+
id: string;
|
|
5
|
+
source: string;
|
|
6
|
+
type: string;
|
|
7
|
+
time: string;
|
|
8
|
+
subject?: string;
|
|
9
|
+
severity: EventSeverity;
|
|
10
|
+
data: TData;
|
|
11
|
+
message?: string;
|
|
12
|
+
dedupeKey?: string;
|
|
13
|
+
schemaVersion: string;
|
|
14
|
+
metadata: Record<string, unknown>;
|
|
15
|
+
}
|
|
16
|
+
export interface EventInput<TData extends EventData = EventData> {
|
|
17
|
+
id?: string;
|
|
18
|
+
source: string;
|
|
19
|
+
type: string;
|
|
20
|
+
time?: string | Date;
|
|
21
|
+
subject?: string;
|
|
22
|
+
severity?: EventSeverity;
|
|
23
|
+
data?: TData;
|
|
24
|
+
message?: string;
|
|
25
|
+
dedupeKey?: string;
|
|
26
|
+
schemaVersion?: string;
|
|
27
|
+
metadata?: Record<string, unknown>;
|
|
28
|
+
}
|
|
29
|
+
export type StringMatcher = string | string[];
|
|
30
|
+
export interface EventFilter {
|
|
31
|
+
source?: StringMatcher;
|
|
32
|
+
type?: StringMatcher;
|
|
33
|
+
subject?: StringMatcher;
|
|
34
|
+
severity?: StringMatcher;
|
|
35
|
+
data?: Record<string, StringMatcher | number | boolean | null>;
|
|
36
|
+
metadata?: Record<string, StringMatcher | number | boolean | null>;
|
|
37
|
+
}
|
|
38
|
+
export interface RetryPolicy {
|
|
39
|
+
maxAttempts?: number;
|
|
40
|
+
backoffMs?: number;
|
|
41
|
+
multiplier?: number;
|
|
42
|
+
}
|
|
43
|
+
export interface RedactionConfig {
|
|
44
|
+
paths?: string[];
|
|
45
|
+
replacement?: string;
|
|
46
|
+
}
|
|
47
|
+
export interface WebhookTransportConfig {
|
|
48
|
+
url: string;
|
|
49
|
+
secret?: string;
|
|
50
|
+
headers?: Record<string, string>;
|
|
51
|
+
timeoutMs?: number;
|
|
52
|
+
}
|
|
53
|
+
export interface CommandTransportConfig {
|
|
54
|
+
command: string;
|
|
55
|
+
args?: string[];
|
|
56
|
+
cwd?: string;
|
|
57
|
+
env?: Record<string, string>;
|
|
58
|
+
timeoutMs?: number;
|
|
59
|
+
}
|
|
60
|
+
export type TransportKind = "webhook" | "command" | "email" | "sse" | "mcp-relay";
|
|
61
|
+
export interface ChannelConfig {
|
|
62
|
+
id: string;
|
|
63
|
+
name?: string;
|
|
64
|
+
enabled: boolean;
|
|
65
|
+
transport: TransportKind;
|
|
66
|
+
filters?: EventFilter[];
|
|
67
|
+
webhook?: WebhookTransportConfig;
|
|
68
|
+
command?: CommandTransportConfig;
|
|
69
|
+
retry?: RetryPolicy;
|
|
70
|
+
redact?: RedactionConfig;
|
|
71
|
+
createdAt: string;
|
|
72
|
+
updatedAt: string;
|
|
73
|
+
metadata?: Record<string, unknown>;
|
|
74
|
+
}
|
|
75
|
+
export interface DeliveryAttempt {
|
|
76
|
+
attempt: number;
|
|
77
|
+
status: "success" | "failed" | "skipped";
|
|
78
|
+
startedAt: string;
|
|
79
|
+
completedAt: string;
|
|
80
|
+
responseStatus?: number;
|
|
81
|
+
responseBody?: string;
|
|
82
|
+
stdout?: string;
|
|
83
|
+
stderr?: string;
|
|
84
|
+
error?: string;
|
|
85
|
+
nextBackoffMs?: number;
|
|
86
|
+
}
|
|
87
|
+
export interface DeliveryResult {
|
|
88
|
+
id: string;
|
|
89
|
+
eventId: string;
|
|
90
|
+
channelId: string;
|
|
91
|
+
transport: TransportKind;
|
|
92
|
+
status: "success" | "failed" | "skipped";
|
|
93
|
+
attempts: DeliveryAttempt[];
|
|
94
|
+
createdAt: string;
|
|
95
|
+
completedAt: string;
|
|
96
|
+
metadata?: Record<string, unknown>;
|
|
97
|
+
}
|
|
98
|
+
export type EventRedactor = (event: EventEnvelope, channel: ChannelConfig) => EventEnvelope | Promise<EventEnvelope>;
|
|
99
|
+
export interface EmitOptions {
|
|
100
|
+
deliver?: boolean;
|
|
101
|
+
dedupe?: boolean;
|
|
102
|
+
}
|
|
103
|
+
export interface ReplayOptions {
|
|
104
|
+
eventId?: string;
|
|
105
|
+
source?: string;
|
|
106
|
+
type?: string;
|
|
107
|
+
dryRun?: boolean;
|
|
108
|
+
}
|
|
109
|
+
export interface StoredEventsData {
|
|
110
|
+
channels: ChannelConfig[];
|
|
111
|
+
events: EventEnvelope[];
|
|
112
|
+
deliveries: DeliveryResult[];
|
|
113
|
+
}
|
|
114
|
+
export interface EmitResult<TData extends EventData = EventData> {
|
|
115
|
+
event: EventEnvelope<TData>;
|
|
116
|
+
deliveries: DeliveryResult[];
|
|
117
|
+
deduped: boolean;
|
|
118
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
// @bun
|
package/package.json
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@hasna/events",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Shared event envelopes, local subscriptions, and webhook delivery for Hasna open-source apps",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"bin": {
|
|
9
|
+
"events": "./dist/cli/index.js",
|
|
10
|
+
"hasna-events": "./dist/cli/index.js"
|
|
11
|
+
},
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"import": "./dist/index.js"
|
|
16
|
+
},
|
|
17
|
+
"./storage": {
|
|
18
|
+
"types": "./dist/storage.d.ts",
|
|
19
|
+
"import": "./dist/storage.js"
|
|
20
|
+
},
|
|
21
|
+
"./signing": {
|
|
22
|
+
"types": "./dist/signing.d.ts",
|
|
23
|
+
"import": "./dist/signing.js"
|
|
24
|
+
},
|
|
25
|
+
"./filter": {
|
|
26
|
+
"types": "./dist/filter.d.ts",
|
|
27
|
+
"import": "./dist/filter.js"
|
|
28
|
+
},
|
|
29
|
+
"./transports": {
|
|
30
|
+
"types": "./dist/transports.d.ts",
|
|
31
|
+
"import": "./dist/transports.js"
|
|
32
|
+
},
|
|
33
|
+
"./commander": {
|
|
34
|
+
"types": "./dist/commander.d.ts",
|
|
35
|
+
"import": "./dist/commander.js"
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
"files": [
|
|
39
|
+
"dist",
|
|
40
|
+
"README.md",
|
|
41
|
+
"LICENSE"
|
|
42
|
+
],
|
|
43
|
+
"scripts": {
|
|
44
|
+
"build": "rm -rf dist && bun build src/cli/index.ts --outdir dist/cli --target bun && bun build src/index.ts src/storage.ts src/signing.ts src/filter.ts src/transports.ts src/types.ts src/commander.ts --outdir dist --target bun && tsc -p tsconfig.build.json --emitDeclarationOnly --outDir dist",
|
|
45
|
+
"typecheck": "tsc --noEmit",
|
|
46
|
+
"test": "bun test",
|
|
47
|
+
"prepublishOnly": "bun run test && bun run build"
|
|
48
|
+
},
|
|
49
|
+
"keywords": [
|
|
50
|
+
"events",
|
|
51
|
+
"webhooks",
|
|
52
|
+
"cli",
|
|
53
|
+
"typescript",
|
|
54
|
+
"bun",
|
|
55
|
+
"hasna"
|
|
56
|
+
],
|
|
57
|
+
"publishConfig": {
|
|
58
|
+
"registry": "https://registry.npmjs.org",
|
|
59
|
+
"access": "public"
|
|
60
|
+
},
|
|
61
|
+
"repository": {
|
|
62
|
+
"type": "git",
|
|
63
|
+
"url": "git+https://github.com/hasna/events.git"
|
|
64
|
+
},
|
|
65
|
+
"homepage": "https://github.com/hasna/events",
|
|
66
|
+
"bugs": {
|
|
67
|
+
"url": "https://github.com/hasna/events/issues"
|
|
68
|
+
},
|
|
69
|
+
"engines": {
|
|
70
|
+
"bun": ">=1.0.0"
|
|
71
|
+
},
|
|
72
|
+
"author": "Hasna",
|
|
73
|
+
"license": "Apache-2.0",
|
|
74
|
+
"dependencies": {
|
|
75
|
+
"commander": "^13.1.0"
|
|
76
|
+
},
|
|
77
|
+
"devDependencies": {
|
|
78
|
+
"@types/bun": "latest",
|
|
79
|
+
"typescript": "^5.7.3"
|
|
80
|
+
}
|
|
81
|
+
}
|