@hasna/events 0.1.16 → 0.1.18

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/signing.js CHANGED
@@ -33,10 +33,10 @@ function verifyWebhookSignature(secret, timestamp, body, signature, options = {}
33
33
  return verifyPayloadSignature(secret, timestamp, body, signature);
34
34
  }
35
35
  export {
36
- DEFAULT_SIGNATURE_TOLERANCE_MS,
37
- buildSignatureBase,
38
- isTimestampWithinTolerance,
39
- signPayload,
36
+ verifyWebhookSignature,
40
37
  verifyPayloadSignature,
41
- verifyWebhookSignature
38
+ signPayload,
39
+ isTimestampWithinTolerance,
40
+ buildSignatureBase,
41
+ DEFAULT_SIGNATURE_TOLERANCE_MS
42
42
  };
package/dist/ssrf.js ADDED
@@ -0,0 +1,222 @@
1
+ // @bun
2
+ // src/ssrf.ts
3
+ import { lookup as dnsLookup } from "dns/promises";
4
+ import { isIP } from "net";
5
+ var DEFAULT_MAX_REDIRECTS = 5;
6
+ var IPV4_PRIVATE_RANGES = [
7
+ [0, 16777215],
8
+ [167772160, 184549375],
9
+ [1681915904, 1686110207],
10
+ [2130706432, 2147483647],
11
+ [2851995648, 2852061183],
12
+ [2886729728, 2887778303],
13
+ [3221225472, 3221225727],
14
+ [3221225984, 3221226239],
15
+ [3227017984, 3227018239],
16
+ [3232235520, 3232301055],
17
+ [3323068416, 3323199487],
18
+ [3325256704, 3325256959],
19
+ [3405803776, 3405804031],
20
+ [3758096384, 4294967295]
21
+ ];
22
+ var IPV6_SPECIAL_PREFIXES = [
23
+ { groups: [0, 0, 0, 0, 0, 0, 0, 0], bits: 128 },
24
+ { groups: [0, 0, 0, 0, 0, 0, 0, 1], bits: 128 },
25
+ { groups: [0, 0, 0, 0, 0, 65535, 0, 0], bits: 96 },
26
+ { groups: [100, 65435, 0, 0, 0, 0, 0, 0], bits: 96 },
27
+ { groups: [256, 0, 0, 0, 0, 0, 0, 0], bits: 64 },
28
+ { groups: [8193, 0, 0, 0, 0, 0, 0, 0], bits: 32 },
29
+ { groups: [8193, 2, 0, 0, 0, 0, 0, 0], bits: 48 },
30
+ { groups: [8193, 16, 0, 0, 0, 0, 0, 0], bits: 28 },
31
+ { groups: [8193, 3512, 0, 0, 0, 0, 0, 0], bits: 32 },
32
+ { groups: [8194, 0, 0, 0, 0, 0, 0, 0], bits: 16 },
33
+ { groups: [16383, 0, 0, 0, 0, 0, 0, 0], bits: 20 },
34
+ { groups: [64512, 0, 0, 0, 0, 0, 0, 0], bits: 7 },
35
+ { groups: [65152, 0, 0, 0, 0, 0, 0, 0], bits: 10 },
36
+ { groups: [65216, 0, 0, 0, 0, 0, 0, 0], bits: 10 },
37
+ { groups: [65280, 0, 0, 0, 0, 0, 0, 0], bits: 8 }
38
+ ];
39
+ function isPrivateAddress(address) {
40
+ const normalized = stripZoneId(address);
41
+ const version = isIP(normalized);
42
+ if (version === 4) {
43
+ const integer = ipv4ToInt(normalized);
44
+ if (integer === undefined)
45
+ return true;
46
+ return IPV4_PRIVATE_RANGES.some(([low, high]) => integer >= low && integer <= high);
47
+ }
48
+ if (version === 6) {
49
+ const groups = ipv6Groups(normalized);
50
+ if (!groups)
51
+ return true;
52
+ for (const prefix of IPV6_SPECIAL_PREFIXES) {
53
+ if (!ipv6MatchesPrefix(groups, prefix.groups, prefix.bits))
54
+ continue;
55
+ if (prefix.bits === 96 && groups[5] === 65535) {
56
+ return isPrivateAddress(ipv4IntToString(groups[6] << 16 | groups[7]));
57
+ }
58
+ if (prefix.bits === 16 && groups[0] === 8194) {
59
+ return isPrivateAddress(ipv4IntToString(groups[1] << 16 | groups[2]));
60
+ }
61
+ return true;
62
+ }
63
+ return false;
64
+ }
65
+ return true;
66
+ }
67
+ async function resolveWebhookTarget(url, policy = {}) {
68
+ const hostname = normalizeHostname(url.hostname);
69
+ const allowlist = (policy.allowPrivateHosts ?? []).map((entry) => normalizeHostname(entry.toLowerCase()));
70
+ if (allowlist.includes(hostname)) {
71
+ const version2 = isIP(hostname);
72
+ if (version2 === 4 || version2 === 6) {
73
+ return { hostname, addresses: [hostname] };
74
+ }
75
+ const lookup2 = policy.lookup ?? defaultTargetLookup;
76
+ let resolved2;
77
+ try {
78
+ resolved2 = await lookup2(hostname);
79
+ } catch {
80
+ throw new Error(`Webhook target ${hostname} could not be resolved`);
81
+ }
82
+ if (!Array.isArray(resolved2) || resolved2.length === 0) {
83
+ throw new Error(`Webhook target ${hostname} resolved to no addresses`);
84
+ }
85
+ const addresses = resolved2.map((entry) => normalizeHostname(entry.address));
86
+ return { hostname, addresses };
87
+ }
88
+ const version = isIP(hostname);
89
+ if (version === 4 || version === 6) {
90
+ if (isPrivateAddress(hostname)) {
91
+ throw new Error(`Webhook target ${hostname} is a private or special-use address`);
92
+ }
93
+ return { hostname, addresses: [hostname] };
94
+ }
95
+ const lookup = policy.lookup ?? defaultTargetLookup;
96
+ let resolved;
97
+ try {
98
+ resolved = await lookup(hostname);
99
+ } catch {
100
+ throw new Error(`Webhook target ${hostname} could not be resolved`);
101
+ }
102
+ if (!Array.isArray(resolved) || resolved.length === 0) {
103
+ throw new Error(`Webhook target ${hostname} resolved to no addresses`);
104
+ }
105
+ const allowed = [];
106
+ for (const entry of resolved) {
107
+ const address = normalizeHostname(entry.address);
108
+ if (isPrivateAddress(address)) {
109
+ if (allowlist.includes(address)) {
110
+ allowed.push(address);
111
+ continue;
112
+ }
113
+ throw new Error(`Webhook target ${hostname} resolves to private or special-use address ${address}`);
114
+ }
115
+ allowed.push(address);
116
+ }
117
+ if (allowed.length === 0) {
118
+ throw new Error(`Webhook target ${hostname} resolved to no public addresses`);
119
+ }
120
+ return { hostname, addresses: allowed };
121
+ }
122
+ async function assertWebhookTargetAllowed(url, policy = {}) {
123
+ await resolveWebhookTarget(url, policy);
124
+ }
125
+ function normalizeMaxRedirects(value) {
126
+ if (value === undefined)
127
+ return DEFAULT_MAX_REDIRECTS;
128
+ if (!Number.isInteger(value) || value < 0)
129
+ throw new Error("webhookTargetPolicy.maxRedirects must be a non-negative integer");
130
+ return value;
131
+ }
132
+ var defaultTargetLookup = async (hostname) => {
133
+ return dnsLookup(hostname, { all: true, verbatim: false });
134
+ };
135
+ function normalizeHostname(hostname) {
136
+ const lower = hostname.toLowerCase();
137
+ if (lower.startsWith("[") && lower.endsWith("]"))
138
+ return lower.slice(1, -1);
139
+ return lower;
140
+ }
141
+ function stripZoneId(address) {
142
+ const percent = address.indexOf("%");
143
+ return percent === -1 ? address : address.slice(0, percent);
144
+ }
145
+ function ipv4ToInt(address) {
146
+ const parts = address.split(".");
147
+ if (parts.length !== 4)
148
+ return;
149
+ let value = 0;
150
+ for (const part of parts) {
151
+ if (!/^\d{1,3}$/.test(part))
152
+ return;
153
+ const octet = Number(part);
154
+ if (octet > 255)
155
+ return;
156
+ value = value << 8 | octet;
157
+ }
158
+ return value >>> 0;
159
+ }
160
+ function ipv4IntToString(integer) {
161
+ return [
162
+ integer >>> 24 & 255,
163
+ integer >>> 16 & 255,
164
+ integer >>> 8 & 255,
165
+ integer & 255
166
+ ].join(".");
167
+ }
168
+ function ipv6Groups(address) {
169
+ const raw = stripZoneId(address);
170
+ const doubleColon = raw.indexOf("::");
171
+ const headText = doubleColon === -1 ? raw : raw.slice(0, doubleColon);
172
+ const tailText = doubleColon === -1 ? "" : raw.slice(doubleColon + 2);
173
+ const parseGroups = (text) => {
174
+ if (text === "")
175
+ return [];
176
+ const out = [];
177
+ for (const part of text.split(":")) {
178
+ if (part.includes(".")) {
179
+ const v4 = ipv4ToInt(part);
180
+ if (v4 === undefined)
181
+ return;
182
+ out.push(v4 >>> 16 & 65535, v4 & 65535);
183
+ } else {
184
+ if (!/^[0-9a-fA-F]{1,4}$/.test(part))
185
+ return;
186
+ out.push(parseInt(part, 16));
187
+ }
188
+ }
189
+ return out;
190
+ };
191
+ const head = parseGroups(headText);
192
+ if (!head)
193
+ return;
194
+ const tail = parseGroups(tailText);
195
+ if (!tail)
196
+ return;
197
+ const total = head.length + tail.length;
198
+ if (doubleColon === -1) {
199
+ return total === 8 ? head : undefined;
200
+ }
201
+ if (total >= 8)
202
+ return;
203
+ return [...head, ...new Array(8 - total).fill(0), ...tail];
204
+ }
205
+ function ipv6MatchesPrefix(groups, prefixGroups, prefixBits) {
206
+ let remaining = prefixBits;
207
+ for (let index = 0;index < prefixGroups.length && remaining > 0; index += 1) {
208
+ const take = Math.min(16, remaining);
209
+ const mask = 65535 << 16 - take & 65535;
210
+ if ((groups[index] & mask) !== (prefixGroups[index] & mask))
211
+ return false;
212
+ remaining -= take;
213
+ }
214
+ return true;
215
+ }
216
+ export {
217
+ resolveWebhookTarget,
218
+ normalizeMaxRedirects,
219
+ isPrivateAddress,
220
+ assertWebhookTargetAllowed,
221
+ DEFAULT_MAX_REDIRECTS
222
+ };
package/dist/storage.js CHANGED
@@ -2,16 +2,114 @@
2
2
  // src/storage.ts
3
3
  import { chmod, mkdir, readFile, rename, writeFile } from "fs/promises";
4
4
  import { Buffer } from "buffer";
5
+ import { existsSync as existsSync2 } from "fs";
6
+ import { join as join2 } from "path";
7
+
8
+ // src/app-home.ts
5
9
  import { existsSync } from "fs";
6
10
  import { homedir } from "os";
7
- import { join } from "path";
11
+ import { join, resolve } from "path";
12
+ import { homedir as pathsResolverHomedir } from "os";
13
+ import { join as pathsResolverJoin } from "path";
14
+ var PATHS_RESOLVER_KIND_ENV = {
15
+ config: "HASNA_CONFIG_HOME",
16
+ data: "HASNA_DATA_HOME",
17
+ state: "HASNA_STATE_HOME",
18
+ cache: "HASNA_CACHE_HOME"
19
+ };
20
+ var PATHS_RESOLVER_APP_SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
21
+ function pathsResolverAssertApp(app) {
22
+ if (typeof app !== "string" || app.length === 0) {
23
+ throw new TypeError("paths: app must be a non-empty string");
24
+ }
25
+ if (!PATHS_RESOLVER_APP_SLUG_RE.test(app)) {
26
+ throw new TypeError(`paths: invalid app slug "${app}" \u2014 expected lowercase kebab-case ([a-z0-9]+(-[a-z0-9]+)*)`);
27
+ }
28
+ }
29
+ function pathsResolverAssertKind(kind) {
30
+ if (!Object.keys(PATHS_RESOLVER_KIND_ENV).includes(kind)) {
31
+ throw new TypeError(`paths: invalid path kind "${kind}" \u2014 expected one of ${Object.keys(PATHS_RESOLVER_KIND_ENV).join(", ")}`);
32
+ }
33
+ }
34
+ function pathsResolverBaseDir(kind, options) {
35
+ pathsResolverAssertKind(kind);
36
+ const env = options.env ?? process.env;
37
+ const override = env[PATHS_RESOLVER_KIND_ENV[kind]];
38
+ if (typeof override === "string" && override.length > 0)
39
+ return override;
40
+ const home = options.home ?? pathsResolverHomedir();
41
+ const platform = options.platform ?? process.platform;
42
+ if (platform === "darwin") {
43
+ switch (kind) {
44
+ case "config":
45
+ case "data":
46
+ return pathsResolverJoin(home, "Library", "Application Support", "Hasna");
47
+ case "cache":
48
+ return pathsResolverJoin(home, "Library", "Caches", "Hasna");
49
+ case "state":
50
+ return pathsResolverJoin(home, "Library", "Logs", "Hasna");
51
+ }
52
+ }
53
+ switch (kind) {
54
+ case "config":
55
+ return pathsResolverJoin(home, ".config", "hasna");
56
+ case "data":
57
+ return pathsResolverJoin(home, ".local", "share", "hasna");
58
+ case "state":
59
+ return pathsResolverJoin(home, ".local", "state", "hasna");
60
+ case "cache":
61
+ return pathsResolverJoin(home, ".cache", "hasna");
62
+ }
63
+ }
64
+ function pathsResolverResolve(kind, options) {
65
+ pathsResolverAssertApp(options.app);
66
+ const appSegment = options.internal === true ? pathsResolverJoin("internal", options.app) : options.app;
67
+ return pathsResolverJoin(pathsResolverBaseDir(kind, options), appSegment);
68
+ }
69
+ function dataDir(options) {
70
+ return pathsResolverResolve("data", options);
71
+ }
8
72
  var HASNA_EVENTS_DIR_ENV = "HASNA_EVENTS_DIR";
9
73
  var HASNA_EVENTS_HOME_ENV = "HASNA_EVENTS_HOME";
74
+ var EVENTS_STORE_SENTINEL_FILE = "events.json";
75
+ function effectiveHome() {
76
+ return process.env["HOME"] || process.env["USERPROFILE"] || homedir();
77
+ }
78
+ function legacyHomeDir() {
79
+ return join(effectiveHome(), ".hasna", "events");
80
+ }
81
+ function resolverHome() {
82
+ return dataDir({ app: "events", home: effectiveHome() || undefined });
83
+ }
84
+ function adoptResolverHome(resolved, env = process.env) {
85
+ const dataOverride = env.HASNA_DATA_HOME;
86
+ if (typeof dataOverride === "string" && dataOverride.trim().length > 0)
87
+ return true;
88
+ return existsSync(join(resolved, EVENTS_STORE_SENTINEL_FILE));
89
+ }
90
+ function exactEventsHome() {
91
+ const dir = process.env[HASNA_EVENTS_DIR_ENV];
92
+ if (dir && dir.trim())
93
+ return dir.trim();
94
+ const home = process.env[HASNA_EVENTS_HOME_ENV];
95
+ if (home && home.trim())
96
+ return home.trim();
97
+ return;
98
+ }
99
+ function getEventsHome() {
100
+ const exact = exactEventsHome();
101
+ if (exact)
102
+ return resolve(exact);
103
+ const resolved = resolverHome();
104
+ return adoptResolverHome(resolved) ? resolve(resolved) : resolve(legacyHomeDir());
105
+ }
106
+
107
+ // src/storage.ts
10
108
  var LOCAL_JSON_EVENT_CURSOR_PREFIX = "local-json-v1:";
11
109
  var DEFAULT_EVENT_PAGE_LIMIT = 100;
12
110
  var MAX_EVENT_PAGE_LIMIT = 1000;
13
111
  function getEventsDataDir(override) {
14
- return override || process.env[HASNA_EVENTS_DIR_ENV] || process.env[HASNA_EVENTS_HOME_ENV] || join(homedir(), ".hasna", "events");
112
+ return override || getEventsHome();
15
113
  }
16
114
  function getActiveEventsDirEnv() {
17
115
  if (process.env[HASNA_EVENTS_DIR_ENV])
@@ -27,12 +125,12 @@ class JsonEventsStore {
27
125
  channelsPath;
28
126
  eventsPath;
29
127
  deliveriesPath;
30
- constructor(dataDir = getEventsDataDir()) {
31
- this.dataDir = dataDir;
32
- this.runtime = localJsonRuntime(dataDir);
33
- this.channelsPath = join(dataDir, "channels.json");
34
- this.eventsPath = join(dataDir, "events.json");
35
- this.deliveriesPath = join(dataDir, "deliveries.json");
128
+ constructor(dataDir2 = getEventsDataDir()) {
129
+ this.dataDir = dataDir2;
130
+ this.runtime = localJsonRuntime(dataDir2);
131
+ this.channelsPath = join2(dataDir2, "channels.json");
132
+ this.eventsPath = join2(dataDir2, "events.json");
133
+ this.deliveriesPath = join2(dataDir2, "deliveries.json");
36
134
  }
37
135
  async init() {
38
136
  await mkdir(this.dataDir, { recursive: true, mode: 448 });
@@ -149,7 +247,7 @@ class JsonEventsStore {
149
247
  };
150
248
  }
151
249
  async ensureArrayFile(path) {
152
- if (!existsSync(path)) {
250
+ if (!existsSync2(path)) {
153
251
  await writeFile(path, `[]
154
252
  `, { encoding: "utf-8", mode: 384 });
155
253
  }
@@ -179,7 +277,7 @@ class JsonEventsStore {
179
277
  });
180
278
  }
181
279
  }
182
- function localJsonRuntime(dataDir = getEventsDataDir()) {
280
+ function localJsonRuntime(dataDir2 = getEventsDataDir()) {
183
281
  return {
184
282
  mode: "local-files",
185
283
  name: "json-events-store",
@@ -192,7 +290,7 @@ function localJsonRuntime(dataDir = getEventsDataDir()) {
192
290
  durable: true,
193
291
  idempotency: "best-effort-local",
194
292
  replayCursors: true,
195
- description: `Local JSON files in ${dataDir}; no SQLite, Postgres, S3, or AWS runtime is configured by this store.`
293
+ description: `Local JSON files in ${dataDir2}; no SQLite, Postgres, S3, or AWS runtime is configured by this store.`
196
294
  };
197
295
  }
198
296
  function encodeLocalJsonEventCursor(offset, options = {}) {
@@ -256,8 +354,8 @@ function assertCursorFilter(name, cursorValue, optionValue) {
256
354
  function findEventByIdentity(events, identity) {
257
355
  return events.find((event) => identity.id !== undefined && event.id === identity.id || identity.dedupeKey !== undefined && event.dedupeKey === identity.dedupeKey);
258
356
  }
259
- async function getEventsStatus(dataDir) {
260
- const store = new JsonEventsStore(dataDir);
357
+ async function getEventsStatus(dataDir2) {
358
+ const store = new JsonEventsStore(dataDir2);
261
359
  await store.init();
262
360
  const [channels, events, deliveries] = await Promise.all([
263
361
  store.listChannels(),
@@ -299,22 +397,22 @@ async function getEventsStatus(dataDir) {
299
397
  }
300
398
  };
301
399
  }
302
- function statusFile(dataDir, fileName, records) {
303
- const path = join(dataDir, fileName);
304
- return { path, exists: existsSync(path), records };
400
+ function statusFile(dataDir2, fileName, records) {
401
+ const path = join2(dataDir2, fileName);
402
+ return { path, exists: existsSync2(path), records };
305
403
  }
306
404
  export {
307
- DEFAULT_EVENT_PAGE_LIMIT,
308
- HASNA_EVENTS_DIR_ENV,
309
- HASNA_EVENTS_HOME_ENV,
310
- JsonEventsStore,
311
- LOCAL_JSON_EVENT_CURSOR_PREFIX,
312
- MAX_EVENT_PAGE_LIMIT,
313
- decodeLocalJsonEventCursor,
314
- encodeLocalJsonEventCursor,
315
- getActiveEventsDirEnv,
316
- getEventsDataDir,
317
- getEventsStatus,
405
+ normalizeEventPageLimit,
318
406
  localJsonRuntime,
319
- normalizeEventPageLimit
407
+ getEventsStatus,
408
+ getEventsDataDir,
409
+ getActiveEventsDirEnv,
410
+ encodeLocalJsonEventCursor,
411
+ decodeLocalJsonEventCursor,
412
+ MAX_EVENT_PAGE_LIMIT,
413
+ LOCAL_JSON_EVENT_CURSOR_PREFIX,
414
+ JsonEventsStore,
415
+ HASNA_EVENTS_HOME_ENV,
416
+ HASNA_EVENTS_DIR_ENV,
417
+ DEFAULT_EVENT_PAGE_LIMIT
320
418
  };