@hasna/events 0.1.0 → 0.1.2

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/dist/cli/index.js CHANGED
@@ -53,7 +53,7 @@ function channelMatchesEvent(channel, event) {
53
53
  }
54
54
 
55
55
  // src/storage.ts
56
- import { mkdir, readFile, rename, writeFile } from "fs/promises";
56
+ import { chmod, mkdir, readFile, rename, writeFile } from "fs/promises";
57
57
  import { existsSync } from "fs";
58
58
  import { homedir } from "os";
59
59
  import { join } from "path";
@@ -75,7 +75,10 @@ class JsonEventsStore {
75
75
  this.deliveriesPath = join(dataDir, "deliveries.json");
76
76
  }
77
77
  async init() {
78
- await mkdir(this.dataDir, { recursive: true });
78
+ await mkdir(this.dataDir, { recursive: true, mode: 448 });
79
+ await chmod(this.dataDir, 448).catch(() => {
80
+ return;
81
+ });
79
82
  await this.ensureArrayFile(this.channelsPath);
80
83
  await this.ensureArrayFile(this.eventsPath);
81
84
  await this.ensureArrayFile(this.deliveriesPath);
@@ -143,8 +146,11 @@ class JsonEventsStore {
143
146
  async ensureArrayFile(path) {
144
147
  if (!existsSync(path)) {
145
148
  await writeFile(path, `[]
146
- `, "utf-8");
149
+ `, { encoding: "utf-8", mode: 384 });
147
150
  }
151
+ await chmod(path, 384).catch(() => {
152
+ return;
153
+ });
148
154
  }
149
155
  async readJson(path, fallback) {
150
156
  try {
@@ -161,8 +167,11 @@ class JsonEventsStore {
161
167
  async writeJson(path, value) {
162
168
  const tempPath = `${path}.${process.pid}.${Date.now()}.tmp`;
163
169
  await writeFile(tempPath, `${JSON.stringify(value, null, 2)}
164
- `, "utf-8");
170
+ `, { encoding: "utf-8", mode: 384 });
165
171
  await rename(tempPath, path);
172
+ await chmod(path, 384).catch(() => {
173
+ return;
174
+ });
166
175
  }
167
176
  }
168
177
 
@@ -172,6 +181,7 @@ import { spawn } from "child_process";
172
181
 
173
182
  // src/signing.ts
174
183
  import { createHmac, timingSafeEqual } from "crypto";
184
+ var DEFAULT_SIGNATURE_TOLERANCE_MS = 5 * 60 * 1000;
175
185
  function buildSignatureBase(timestamp, body) {
176
186
  return `${timestamp}.${body}`;
177
187
  }
@@ -371,7 +381,7 @@ class EventsClient {
371
381
  return this.store.removeChannel(id);
372
382
  }
373
383
  async emit(input, options = {}) {
374
- const event = createEvent(input);
384
+ const event = options.redactSensitiveData === false ? createEvent(input) : redactSensitiveKeys(createEvent(input));
375
385
  if (options.dedupe !== false) {
376
386
  const existing = await this.store.findEventByIdentity({ id: input.id, dedupeKey: event.dedupeKey });
377
387
  if (existing) {
@@ -486,9 +496,22 @@ function sanitizeChannelForOutput(channel) {
486
496
  function sanitizeChannelsForOutput(channels) {
487
497
  return channels.map(sanitizeChannelForOutput);
488
498
  }
499
+ function redactSensitiveKeys(event, replacement = "[REDACTED]") {
500
+ return redactValue(event, replacement);
501
+ }
489
502
  function shouldRedactKey(key) {
490
503
  return /secret|token|password|api[_-]?key|authorization/i.test(key);
491
504
  }
505
+ function redactValue(value, replacement) {
506
+ if (Array.isArray(value))
507
+ return value.map((item) => redactValue(item, replacement));
508
+ if (!value || typeof value !== "object")
509
+ return value;
510
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [
511
+ key,
512
+ shouldRedactKey(key) ? replacement : redactValue(item, replacement)
513
+ ]));
514
+ }
492
515
  function setPath(input, path, replacement) {
493
516
  const parts = path.split(".");
494
517
  let cursor = input;
package/dist/commander.js CHANGED
@@ -43,7 +43,7 @@ function channelMatchesEvent(channel, event) {
43
43
  }
44
44
 
45
45
  // src/storage.ts
46
- import { mkdir, readFile, rename, writeFile } from "fs/promises";
46
+ import { chmod, mkdir, readFile, rename, writeFile } from "fs/promises";
47
47
  import { existsSync } from "fs";
48
48
  import { homedir } from "os";
49
49
  import { join } from "path";
@@ -65,7 +65,10 @@ class JsonEventsStore {
65
65
  this.deliveriesPath = join(dataDir, "deliveries.json");
66
66
  }
67
67
  async init() {
68
- await mkdir(this.dataDir, { recursive: true });
68
+ await mkdir(this.dataDir, { recursive: true, mode: 448 });
69
+ await chmod(this.dataDir, 448).catch(() => {
70
+ return;
71
+ });
69
72
  await this.ensureArrayFile(this.channelsPath);
70
73
  await this.ensureArrayFile(this.eventsPath);
71
74
  await this.ensureArrayFile(this.deliveriesPath);
@@ -133,8 +136,11 @@ class JsonEventsStore {
133
136
  async ensureArrayFile(path) {
134
137
  if (!existsSync(path)) {
135
138
  await writeFile(path, `[]
136
- `, "utf-8");
139
+ `, { encoding: "utf-8", mode: 384 });
137
140
  }
141
+ await chmod(path, 384).catch(() => {
142
+ return;
143
+ });
138
144
  }
139
145
  async readJson(path, fallback) {
140
146
  try {
@@ -151,13 +157,17 @@ class JsonEventsStore {
151
157
  async writeJson(path, value) {
152
158
  const tempPath = `${path}.${process.pid}.${Date.now()}.tmp`;
153
159
  await writeFile(tempPath, `${JSON.stringify(value, null, 2)}
154
- `, "utf-8");
160
+ `, { encoding: "utf-8", mode: 384 });
155
161
  await rename(tempPath, path);
162
+ await chmod(path, 384).catch(() => {
163
+ return;
164
+ });
156
165
  }
157
166
  }
158
167
 
159
168
  // src/signing.ts
160
169
  import { createHmac, timingSafeEqual } from "crypto";
170
+ var DEFAULT_SIGNATURE_TOLERANCE_MS = 5 * 60 * 1000;
161
171
  function buildSignatureBase(timestamp, body) {
162
172
  return `${timestamp}.${body}`;
163
173
  }
@@ -182,7 +192,8 @@ function isTimestampWithinTolerance(timestamp, toleranceMs, now = Date.now()) {
182
192
  return Math.abs(reference - parsed) <= toleranceMs;
183
193
  }
184
194
  function verifyWebhookSignature(secret, timestamp, body, signature, options = {}) {
185
- if (options.toleranceMs !== undefined && !isTimestampWithinTolerance(timestamp, options.toleranceMs, options.now)) {
195
+ const toleranceMs = options.toleranceMs ?? DEFAULT_SIGNATURE_TOLERANCE_MS;
196
+ if (!isTimestampWithinTolerance(timestamp, toleranceMs, options.now)) {
186
197
  return false;
187
198
  }
188
199
  return verifyPayloadSignature(secret, timestamp, body, signature);
@@ -381,7 +392,7 @@ class EventsClient {
381
392
  return this.store.removeChannel(id);
382
393
  }
383
394
  async emit(input, options = {}) {
384
- const event = createEvent(input);
395
+ const event = options.redactSensitiveData === false ? createEvent(input) : redactSensitiveKeys(createEvent(input));
385
396
  if (options.dedupe !== false) {
386
397
  const existing = await this.store.findEventByIdentity({ id: input.id, dedupeKey: event.dedupeKey });
387
398
  if (existing) {
@@ -496,9 +507,22 @@ function sanitizeChannelForOutput(channel) {
496
507
  function sanitizeChannelsForOutput(channels) {
497
508
  return channels.map(sanitizeChannelForOutput);
498
509
  }
510
+ function redactSensitiveKeys(event, replacement = "[REDACTED]") {
511
+ return redactValue(event, replacement);
512
+ }
499
513
  function shouldRedactKey(key) {
500
514
  return /secret|token|password|api[_-]?key|authorization/i.test(key);
501
515
  }
516
+ function redactValue(value, replacement) {
517
+ if (Array.isArray(value))
518
+ return value.map((item) => redactValue(item, replacement));
519
+ if (!value || typeof value !== "object")
520
+ return value;
521
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [
522
+ key,
523
+ shouldRedactKey(key) ? replacement : redactValue(item, replacement)
524
+ ]));
525
+ }
502
526
  function setPath(input, path, replacement) {
503
527
  const parts = path.split(".");
504
528
  let cursor = input;
package/dist/index.d.ts CHANGED
@@ -35,3 +35,4 @@ export declare class EventsClient {
35
35
  export declare function redactPaths<T extends EventEnvelope>(event: T, paths: string[], replacement?: string): T;
36
36
  export declare function sanitizeChannelForOutput(channel: ChannelConfig): ChannelConfig;
37
37
  export declare function sanitizeChannelsForOutput(channels: ChannelConfig[]): ChannelConfig[];
38
+ export declare function redactSensitiveKeys<T extends EventEnvelope>(event: T, replacement?: string): T;
package/dist/index.js CHANGED
@@ -43,7 +43,7 @@ function channelMatchesEvent(channel, event) {
43
43
  }
44
44
 
45
45
  // src/storage.ts
46
- import { mkdir, readFile, rename, writeFile } from "fs/promises";
46
+ import { chmod, mkdir, readFile, rename, writeFile } from "fs/promises";
47
47
  import { existsSync } from "fs";
48
48
  import { homedir } from "os";
49
49
  import { join } from "path";
@@ -65,7 +65,10 @@ class JsonEventsStore {
65
65
  this.deliveriesPath = join(dataDir, "deliveries.json");
66
66
  }
67
67
  async init() {
68
- await mkdir(this.dataDir, { recursive: true });
68
+ await mkdir(this.dataDir, { recursive: true, mode: 448 });
69
+ await chmod(this.dataDir, 448).catch(() => {
70
+ return;
71
+ });
69
72
  await this.ensureArrayFile(this.channelsPath);
70
73
  await this.ensureArrayFile(this.eventsPath);
71
74
  await this.ensureArrayFile(this.deliveriesPath);
@@ -133,8 +136,11 @@ class JsonEventsStore {
133
136
  async ensureArrayFile(path) {
134
137
  if (!existsSync(path)) {
135
138
  await writeFile(path, `[]
136
- `, "utf-8");
139
+ `, { encoding: "utf-8", mode: 384 });
137
140
  }
141
+ await chmod(path, 384).catch(() => {
142
+ return;
143
+ });
138
144
  }
139
145
  async readJson(path, fallback) {
140
146
  try {
@@ -151,13 +157,17 @@ class JsonEventsStore {
151
157
  async writeJson(path, value) {
152
158
  const tempPath = `${path}.${process.pid}.${Date.now()}.tmp`;
153
159
  await writeFile(tempPath, `${JSON.stringify(value, null, 2)}
154
- `, "utf-8");
160
+ `, { encoding: "utf-8", mode: 384 });
155
161
  await rename(tempPath, path);
162
+ await chmod(path, 384).catch(() => {
163
+ return;
164
+ });
156
165
  }
157
166
  }
158
167
 
159
168
  // src/signing.ts
160
169
  import { createHmac, timingSafeEqual } from "crypto";
170
+ var DEFAULT_SIGNATURE_TOLERANCE_MS = 5 * 60 * 1000;
161
171
  function buildSignatureBase(timestamp, body) {
162
172
  return `${timestamp}.${body}`;
163
173
  }
@@ -182,7 +192,8 @@ function isTimestampWithinTolerance(timestamp, toleranceMs, now = Date.now()) {
182
192
  return Math.abs(reference - parsed) <= toleranceMs;
183
193
  }
184
194
  function verifyWebhookSignature(secret, timestamp, body, signature, options = {}) {
185
- if (options.toleranceMs !== undefined && !isTimestampWithinTolerance(timestamp, options.toleranceMs, options.now)) {
195
+ const toleranceMs = options.toleranceMs ?? DEFAULT_SIGNATURE_TOLERANCE_MS;
196
+ if (!isTimestampWithinTolerance(timestamp, toleranceMs, options.now)) {
186
197
  return false;
187
198
  }
188
199
  return verifyPayloadSignature(secret, timestamp, body, signature);
@@ -381,7 +392,7 @@ class EventsClient {
381
392
  return this.store.removeChannel(id);
382
393
  }
383
394
  async emit(input, options = {}) {
384
- const event = createEvent(input);
395
+ const event = options.redactSensitiveData === false ? createEvent(input) : redactSensitiveKeys(createEvent(input));
385
396
  if (options.dedupe !== false) {
386
397
  const existing = await this.store.findEventByIdentity({ id: input.id, dedupeKey: event.dedupeKey });
387
398
  if (existing) {
@@ -496,9 +507,22 @@ function sanitizeChannelForOutput(channel) {
496
507
  function sanitizeChannelsForOutput(channels) {
497
508
  return channels.map(sanitizeChannelForOutput);
498
509
  }
510
+ function redactSensitiveKeys(event, replacement = "[REDACTED]") {
511
+ return redactValue(event, replacement);
512
+ }
499
513
  function shouldRedactKey(key) {
500
514
  return /secret|token|password|api[_-]?key|authorization/i.test(key);
501
515
  }
516
+ function redactValue(value, replacement) {
517
+ if (Array.isArray(value))
518
+ return value.map((item) => redactValue(item, replacement));
519
+ if (!value || typeof value !== "object")
520
+ return value;
521
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [
522
+ key,
523
+ shouldRedactKey(key) ? replacement : redactValue(item, replacement)
524
+ ]));
525
+ }
502
526
  function setPath(input, path, replacement) {
503
527
  const parts = path.split(".");
504
528
  let cursor = input;
@@ -530,6 +554,7 @@ export {
530
554
  signPayload,
531
555
  sanitizeChannelsForOutput,
532
556
  sanitizeChannelForOutput,
557
+ redactSensitiveKeys,
533
558
  redactPaths,
534
559
  matchString,
535
560
  isTimestampWithinTolerance,
@@ -546,5 +571,6 @@ export {
546
571
  JsonEventsStore,
547
572
  HASNA_EVENTS_HOME_ENV,
548
573
  HASNA_EVENTS_DIR_ENV,
549
- EventsClient
574
+ EventsClient,
575
+ DEFAULT_SIGNATURE_TOLERANCE_MS
550
576
  };
package/dist/signing.d.ts CHANGED
@@ -2,6 +2,7 @@ export interface SignatureVerificationOptions {
2
2
  toleranceMs?: number;
3
3
  now?: number | Date;
4
4
  }
5
+ export declare const DEFAULT_SIGNATURE_TOLERANCE_MS: number;
5
6
  export declare function buildSignatureBase(timestamp: string, body: string): string;
6
7
  export declare function signPayload(secret: string, timestamp: string, body: string): string;
7
8
  export declare function verifyPayloadSignature(secret: string, timestamp: string, body: string, signature: string): boolean;
package/dist/signing.js CHANGED
@@ -1,6 +1,7 @@
1
1
  // @bun
2
2
  // src/signing.ts
3
3
  import { createHmac, timingSafeEqual } from "crypto";
4
+ var DEFAULT_SIGNATURE_TOLERANCE_MS = 5 * 60 * 1000;
4
5
  function buildSignatureBase(timestamp, body) {
5
6
  return `${timestamp}.${body}`;
6
7
  }
@@ -25,7 +26,8 @@ function isTimestampWithinTolerance(timestamp, toleranceMs, now = Date.now()) {
25
26
  return Math.abs(reference - parsed) <= toleranceMs;
26
27
  }
27
28
  function verifyWebhookSignature(secret, timestamp, body, signature, options = {}) {
28
- if (options.toleranceMs !== undefined && !isTimestampWithinTolerance(timestamp, options.toleranceMs, options.now)) {
29
+ const toleranceMs = options.toleranceMs ?? DEFAULT_SIGNATURE_TOLERANCE_MS;
30
+ if (!isTimestampWithinTolerance(timestamp, toleranceMs, options.now)) {
29
31
  return false;
30
32
  }
31
33
  return verifyPayloadSignature(secret, timestamp, body, signature);
@@ -35,5 +37,6 @@ export {
35
37
  verifyPayloadSignature,
36
38
  signPayload,
37
39
  isTimestampWithinTolerance,
38
- buildSignatureBase
40
+ buildSignatureBase,
41
+ DEFAULT_SIGNATURE_TOLERANCE_MS
39
42
  };
package/dist/storage.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // @bun
2
2
  // src/storage.ts
3
- import { mkdir, readFile, rename, writeFile } from "fs/promises";
3
+ import { chmod, mkdir, readFile, rename, writeFile } from "fs/promises";
4
4
  import { existsSync } from "fs";
5
5
  import { homedir } from "os";
6
6
  import { join } from "path";
@@ -22,7 +22,10 @@ class JsonEventsStore {
22
22
  this.deliveriesPath = join(dataDir, "deliveries.json");
23
23
  }
24
24
  async init() {
25
- await mkdir(this.dataDir, { recursive: true });
25
+ await mkdir(this.dataDir, { recursive: true, mode: 448 });
26
+ await chmod(this.dataDir, 448).catch(() => {
27
+ return;
28
+ });
26
29
  await this.ensureArrayFile(this.channelsPath);
27
30
  await this.ensureArrayFile(this.eventsPath);
28
31
  await this.ensureArrayFile(this.deliveriesPath);
@@ -90,8 +93,11 @@ class JsonEventsStore {
90
93
  async ensureArrayFile(path) {
91
94
  if (!existsSync(path)) {
92
95
  await writeFile(path, `[]
93
- `, "utf-8");
96
+ `, { encoding: "utf-8", mode: 384 });
94
97
  }
98
+ await chmod(path, 384).catch(() => {
99
+ return;
100
+ });
95
101
  }
96
102
  async readJson(path, fallback) {
97
103
  try {
@@ -108,8 +114,11 @@ class JsonEventsStore {
108
114
  async writeJson(path, value) {
109
115
  const tempPath = `${path}.${process.pid}.${Date.now()}.tmp`;
110
116
  await writeFile(tempPath, `${JSON.stringify(value, null, 2)}
111
- `, "utf-8");
117
+ `, { encoding: "utf-8", mode: 384 });
112
118
  await rename(tempPath, path);
119
+ await chmod(path, 384).catch(() => {
120
+ return;
121
+ });
113
122
  }
114
123
  }
115
124
  export {
@@ -1,6 +1,7 @@
1
1
  // @bun
2
2
  // src/signing.ts
3
3
  import { createHmac, timingSafeEqual } from "crypto";
4
+ var DEFAULT_SIGNATURE_TOLERANCE_MS = 5 * 60 * 1000;
4
5
  function buildSignatureBase(timestamp, body) {
5
6
  return `${timestamp}.${body}`;
6
7
  }
@@ -25,7 +26,8 @@ function isTimestampWithinTolerance(timestamp, toleranceMs, now = Date.now()) {
25
26
  return Math.abs(reference - parsed) <= toleranceMs;
26
27
  }
27
28
  function verifyWebhookSignature(secret, timestamp, body, signature, options = {}) {
28
- if (options.toleranceMs !== undefined && !isTimestampWithinTolerance(timestamp, options.toleranceMs, options.now)) {
29
+ const toleranceMs = options.toleranceMs ?? DEFAULT_SIGNATURE_TOLERANCE_MS;
30
+ if (!isTimestampWithinTolerance(timestamp, toleranceMs, options.now)) {
29
31
  return false;
30
32
  }
31
33
  return verifyPayloadSignature(secret, timestamp, body, signature);
package/dist/types.d.ts CHANGED
@@ -99,6 +99,7 @@ export type EventRedactor = (event: EventEnvelope, channel: ChannelConfig) => Ev
99
99
  export interface EmitOptions {
100
100
  deliver?: boolean;
101
101
  dedupe?: boolean;
102
+ redactSensitiveData?: boolean;
102
103
  }
103
104
  export interface ReplayOptions {
104
105
  eventId?: string;
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "@hasna/events",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Shared event envelopes, local subscriptions, and webhook delivery for Hasna open-source apps",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
7
7
  "types": "./dist/index.d.ts",
8
8
  "bin": {
9
- "events": "./dist/cli/index.js",
10
- "hasna-events": "./dist/cli/index.js"
9
+ "events": "dist/cli/index.js",
10
+ "hasna-events": "dist/cli/index.js"
11
11
  },
12
12
  "exports": {
13
13
  ".": {