@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
package/LICENSE
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
Apache License
|
|
2
|
+
Version 2.0, January 2004
|
|
3
|
+
http://www.apache.org/licenses/
|
|
4
|
+
|
|
5
|
+
Copyright 2026 Hasna
|
|
6
|
+
|
|
7
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
8
|
+
you may not use this file except in compliance with the License.
|
|
9
|
+
You may obtain a copy of the License at
|
|
10
|
+
|
|
11
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
12
|
+
|
|
13
|
+
Unless required by applicable law or agreed to in writing, software
|
|
14
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
15
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
16
|
+
See the License for the specific language governing permissions and
|
|
17
|
+
limitations under the License.
|
package/README.md
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
# @hasna/events
|
|
2
|
+
|
|
3
|
+
Shared event envelopes, subscription config, webhook delivery, and command dispatch for Hasna open-source apps.
|
|
4
|
+
|
|
5
|
+
This package is local-first. By default it stores JSON files under `~/.hasna/events`:
|
|
6
|
+
|
|
7
|
+
- `channels.json`
|
|
8
|
+
- `events.json`
|
|
9
|
+
- `deliveries.json`
|
|
10
|
+
|
|
11
|
+
Override the data directory with `HASNA_EVENTS_DIR`, `HASNA_EVENTS_HOME`, or the CLI `--dir` flag.
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
bun add @hasna/events
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
This package is not published by this repository setup step. Apps can also depend on the local workspace path while the rollout is in progress.
|
|
20
|
+
|
|
21
|
+
## Event Envelope
|
|
22
|
+
|
|
23
|
+
All apps should emit the same stable envelope:
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
import { EventsClient } from "@hasna/events";
|
|
27
|
+
|
|
28
|
+
const events = new EventsClient();
|
|
29
|
+
|
|
30
|
+
await events.emit({
|
|
31
|
+
id: "optional-stable-id",
|
|
32
|
+
source: "tickets",
|
|
33
|
+
type: "ticket.created",
|
|
34
|
+
time: new Date(),
|
|
35
|
+
subject: "ticket:123",
|
|
36
|
+
severity: "notice",
|
|
37
|
+
data: { ticketId: 123 },
|
|
38
|
+
message: "Ticket created",
|
|
39
|
+
dedupeKey: "tickets:ticket:123:created",
|
|
40
|
+
schemaVersion: "1.0",
|
|
41
|
+
metadata: { tenant: "acme" },
|
|
42
|
+
});
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Envelope fields are:
|
|
46
|
+
|
|
47
|
+
- `id`
|
|
48
|
+
- `source`
|
|
49
|
+
- `type`
|
|
50
|
+
- `time`
|
|
51
|
+
- `subject`
|
|
52
|
+
- `severity`
|
|
53
|
+
- `data`
|
|
54
|
+
- `message`
|
|
55
|
+
- `dedupeKey`
|
|
56
|
+
- `schemaVersion`
|
|
57
|
+
- `metadata`
|
|
58
|
+
|
|
59
|
+
`source` should be the emitting app or bounded context. `type` should use dot notation such as `ticket.created`, `repo.synced`, or `check.failed`.
|
|
60
|
+
|
|
61
|
+
## Channels And Filters
|
|
62
|
+
|
|
63
|
+
Channels are reusable subscriptions. They can be enabled or disabled, filtered by source/type/subject/severity, and configured with transport-specific settings.
|
|
64
|
+
|
|
65
|
+
```ts
|
|
66
|
+
await events.addChannel({
|
|
67
|
+
id: "ops-webhook",
|
|
68
|
+
enabled: true,
|
|
69
|
+
transport: "webhook",
|
|
70
|
+
filters: [{ type: "ticket.*", severity: ["warning", "error", "critical"] }],
|
|
71
|
+
webhook: {
|
|
72
|
+
url: "https://example.com/webhooks/hasna",
|
|
73
|
+
secret: process.env.HASNA_WEBHOOK_SECRET,
|
|
74
|
+
},
|
|
75
|
+
retry: {
|
|
76
|
+
maxAttempts: 3,
|
|
77
|
+
backoffMs: 500,
|
|
78
|
+
multiplier: 2,
|
|
79
|
+
},
|
|
80
|
+
redact: {
|
|
81
|
+
paths: ["data.token", "metadata.authorization"],
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Filters support `*` wildcards and nested `data` or `metadata` paths.
|
|
87
|
+
|
|
88
|
+
## Webhook Transport
|
|
89
|
+
|
|
90
|
+
Webhook delivery sends a `POST` with the event envelope as JSON.
|
|
91
|
+
|
|
92
|
+
Headers:
|
|
93
|
+
|
|
94
|
+
- `X-Hasna-Event-Id`
|
|
95
|
+
- `X-Hasna-Event-Type`
|
|
96
|
+
- `X-Hasna-Timestamp`
|
|
97
|
+
- `X-Hasna-Signature` when `webhook.secret` is configured
|
|
98
|
+
|
|
99
|
+
Signatures use HMAC-SHA256 over:
|
|
100
|
+
|
|
101
|
+
```text
|
|
102
|
+
<timestamp>.<json-body>
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
The signature format is:
|
|
106
|
+
|
|
107
|
+
```text
|
|
108
|
+
sha256=<hex digest>
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Consumers can verify with:
|
|
112
|
+
|
|
113
|
+
```ts
|
|
114
|
+
import { verifyPayloadSignature } from "@hasna/events/signing";
|
|
115
|
+
|
|
116
|
+
const ok = verifyPayloadSignature(secret, timestamp, body, signature);
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## Command Transport
|
|
120
|
+
|
|
121
|
+
Command channels run a local process and pass the event on stdin and environment variables.
|
|
122
|
+
|
|
123
|
+
```ts
|
|
124
|
+
await events.addChannel({
|
|
125
|
+
id: "local-handler",
|
|
126
|
+
enabled: true,
|
|
127
|
+
transport: "command",
|
|
128
|
+
filters: [{ type: "repo.*" }],
|
|
129
|
+
command: {
|
|
130
|
+
command: "bun",
|
|
131
|
+
args: ["run", "scripts/handle-event.ts"],
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Environment variables:
|
|
137
|
+
|
|
138
|
+
- `HASNA_CHANNEL_ID`
|
|
139
|
+
- `HASNA_EVENT_ID`
|
|
140
|
+
- `HASNA_EVENT_TYPE`
|
|
141
|
+
- `HASNA_EVENT_SOURCE`
|
|
142
|
+
- `HASNA_EVENT_SUBJECT`
|
|
143
|
+
- `HASNA_EVENT_SEVERITY`
|
|
144
|
+
- `HASNA_EVENT_TIME`
|
|
145
|
+
- `HASNA_EVENT_DEDUPE_KEY`
|
|
146
|
+
- `HASNA_EVENT_SCHEMA_VERSION`
|
|
147
|
+
- `HASNA_EVENT_JSON`
|
|
148
|
+
|
|
149
|
+
The transport type union already reserves `email`, `sse`, and `mcp-relay` for later implementations.
|
|
150
|
+
|
|
151
|
+
## Redaction
|
|
152
|
+
|
|
153
|
+
Use channel-level paths for config-only redaction:
|
|
154
|
+
|
|
155
|
+
```ts
|
|
156
|
+
await events.addChannel({
|
|
157
|
+
id: "secure-hook",
|
|
158
|
+
enabled: true,
|
|
159
|
+
transport: "webhook",
|
|
160
|
+
webhook: { url: "https://example.com" },
|
|
161
|
+
redact: { paths: ["data.secret", "metadata.token"] },
|
|
162
|
+
});
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Use runtime hooks for app-specific policies:
|
|
166
|
+
|
|
167
|
+
```ts
|
|
168
|
+
const events = new EventsClient({
|
|
169
|
+
redactors: [
|
|
170
|
+
async (event) => ({
|
|
171
|
+
...event,
|
|
172
|
+
metadata: { ...event.metadata, internalOnly: undefined },
|
|
173
|
+
}),
|
|
174
|
+
],
|
|
175
|
+
});
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
## CLI
|
|
179
|
+
|
|
180
|
+
The package exposes `events` and `hasna-events`.
|
|
181
|
+
|
|
182
|
+
```bash
|
|
183
|
+
events webhooks add https://example.com/webhooks/hasna \
|
|
184
|
+
--id ops \
|
|
185
|
+
--type "ticket.*" \
|
|
186
|
+
--secret "$HASNA_WEBHOOK_SECRET" \
|
|
187
|
+
--retry-attempts 3 \
|
|
188
|
+
--retry-backoff-ms 500
|
|
189
|
+
|
|
190
|
+
events webhooks list
|
|
191
|
+
events webhooks test ops
|
|
192
|
+
events webhooks remove ops
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
Emit, list, and replay:
|
|
196
|
+
|
|
197
|
+
```bash
|
|
198
|
+
events events emit ticket.created \
|
|
199
|
+
--source tickets \
|
|
200
|
+
--subject ticket:123 \
|
|
201
|
+
--severity notice \
|
|
202
|
+
--message "Ticket created" \
|
|
203
|
+
--data '{"ticketId":123}'
|
|
204
|
+
|
|
205
|
+
events events list --limit 20
|
|
206
|
+
events events replay --type ticket.created
|
|
207
|
+
events events replay --dry-run
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
Use `--json` for script-friendly output and `--dir <path>` for isolated data.
|
|
211
|
+
|
|
212
|
+
## App Integration Pattern
|
|
213
|
+
|
|
214
|
+
Apps should keep event emission near durable state changes and avoid hardcoding app-specific webhooks. The common pattern is:
|
|
215
|
+
|
|
216
|
+
```ts
|
|
217
|
+
import { EventsClient } from "@hasna/events";
|
|
218
|
+
|
|
219
|
+
const events = new EventsClient();
|
|
220
|
+
|
|
221
|
+
export async function recordDomainEvent() {
|
|
222
|
+
await events.emit({
|
|
223
|
+
source: "your-app",
|
|
224
|
+
type: "domain.object.changed",
|
|
225
|
+
subject: "object:123",
|
|
226
|
+
severity: "info",
|
|
227
|
+
data: { id: 123 },
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
Local users and agents can configure channels once through the CLI, and every app using `@hasna/events` will share the same local channel config.
|
|
233
|
+
|
|
234
|
+
## Development
|
|
235
|
+
|
|
236
|
+
```bash
|
|
237
|
+
bun test
|
|
238
|
+
bun run typecheck
|
|
239
|
+
bun run build
|
|
240
|
+
```
|