@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 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
+ ```
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env bun
2
+ export {};