@hasna/events 0.1.16 → 0.1.17

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,121 @@
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 join3 } from "path";
7
+
8
+ // src/app-home.ts
5
9
  import { existsSync } from "fs";
10
+ import { homedir as homedir2 } from "os";
11
+ import { join as join2, resolve } from "path";
12
+
13
+ // ../../node_modules/.bun/@hasna+paths@0.1.0/node_modules/@hasna/paths/dist/index.js
6
14
  import { homedir } from "os";
7
15
  import { join } from "path";
16
+ var KIND_ENV = {
17
+ config: "HASNA_CONFIG_HOME",
18
+ data: "HASNA_DATA_HOME",
19
+ state: "HASNA_STATE_HOME",
20
+ cache: "HASNA_CACHE_HOME"
21
+ };
22
+ var APP_SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
23
+ function assertApp(app) {
24
+ if (typeof app !== "string" || app.length === 0) {
25
+ throw new TypeError("paths: app must be a non-empty string");
26
+ }
27
+ if (!APP_SLUG_RE.test(app)) {
28
+ throw new TypeError(`paths: invalid app slug "${app}" \u2014 expected lowercase kebab-case ([a-z0-9]+(-[a-z0-9]+)*)`);
29
+ }
30
+ }
31
+ function envOf(options) {
32
+ return options.env ?? process.env;
33
+ }
34
+ function envValue(options, kind) {
35
+ const value = envOf(options)[KIND_ENV[kind]];
36
+ return typeof value === "string" && value.length > 0 ? value : undefined;
37
+ }
38
+ function isMacOS(platform) {
39
+ return platform === "darwin";
40
+ }
41
+ function baseDir(kind, options) {
42
+ const override = envValue(options, kind);
43
+ if (override)
44
+ return override;
45
+ const home = options.home ?? homedir();
46
+ const platform = options.platform ?? process.platform;
47
+ if (isMacOS(platform)) {
48
+ switch (kind) {
49
+ case "config":
50
+ case "data":
51
+ return join(home, "Library", "Application Support", "Hasna");
52
+ case "cache":
53
+ return join(home, "Library", "Caches", "Hasna");
54
+ case "state":
55
+ return join(home, "Library", "Logs", "Hasna");
56
+ }
57
+ }
58
+ switch (kind) {
59
+ case "config":
60
+ return join(home, ".config", "hasna");
61
+ case "data":
62
+ return join(home, ".local", "share", "hasna");
63
+ case "state":
64
+ return join(home, ".local", "state", "hasna");
65
+ case "cache":
66
+ return join(home, ".cache", "hasna");
67
+ }
68
+ }
69
+ function resolvePath(kind, options) {
70
+ assertApp(options.app);
71
+ const appSegment = options.internal === true ? join("internal", options.app) : options.app;
72
+ return join(baseDir(kind, options), appSegment);
73
+ }
74
+ function dataDir(options) {
75
+ return resolvePath("data", options);
76
+ }
77
+
78
+ // src/app-home.ts
8
79
  var HASNA_EVENTS_DIR_ENV = "HASNA_EVENTS_DIR";
9
80
  var HASNA_EVENTS_HOME_ENV = "HASNA_EVENTS_HOME";
81
+ var EVENTS_STORE_SENTINEL_FILE = "events.json";
82
+ function effectiveHome() {
83
+ return process.env["HOME"] || process.env["USERPROFILE"] || homedir2();
84
+ }
85
+ function legacyHomeDir() {
86
+ return join2(effectiveHome(), ".hasna", "events");
87
+ }
88
+ function resolverHome() {
89
+ return dataDir({ app: "events", home: effectiveHome() || undefined });
90
+ }
91
+ function adoptResolverHome(resolved, env = process.env) {
92
+ const dataOverride = env.HASNA_DATA_HOME;
93
+ if (typeof dataOverride === "string" && dataOverride.trim().length > 0)
94
+ return true;
95
+ return existsSync(join2(resolved, EVENTS_STORE_SENTINEL_FILE));
96
+ }
97
+ function exactEventsHome() {
98
+ const dir = process.env[HASNA_EVENTS_DIR_ENV];
99
+ if (dir && dir.trim())
100
+ return dir.trim();
101
+ const home = process.env[HASNA_EVENTS_HOME_ENV];
102
+ if (home && home.trim())
103
+ return home.trim();
104
+ return;
105
+ }
106
+ function getEventsHome() {
107
+ const exact = exactEventsHome();
108
+ if (exact)
109
+ return resolve(exact);
110
+ const resolved = resolverHome();
111
+ return adoptResolverHome(resolved) ? resolve(resolved) : resolve(legacyHomeDir());
112
+ }
113
+
114
+ // src/storage.ts
10
115
  var LOCAL_JSON_EVENT_CURSOR_PREFIX = "local-json-v1:";
11
116
  var DEFAULT_EVENT_PAGE_LIMIT = 100;
12
117
  var MAX_EVENT_PAGE_LIMIT = 1000;
13
118
  function getEventsDataDir(override) {
14
- return override || process.env[HASNA_EVENTS_DIR_ENV] || process.env[HASNA_EVENTS_HOME_ENV] || join(homedir(), ".hasna", "events");
119
+ return override || getEventsHome();
15
120
  }
16
121
  function getActiveEventsDirEnv() {
17
122
  if (process.env[HASNA_EVENTS_DIR_ENV])
@@ -27,12 +132,12 @@ class JsonEventsStore {
27
132
  channelsPath;
28
133
  eventsPath;
29
134
  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");
135
+ constructor(dataDir2 = getEventsDataDir()) {
136
+ this.dataDir = dataDir2;
137
+ this.runtime = localJsonRuntime(dataDir2);
138
+ this.channelsPath = join3(dataDir2, "channels.json");
139
+ this.eventsPath = join3(dataDir2, "events.json");
140
+ this.deliveriesPath = join3(dataDir2, "deliveries.json");
36
141
  }
37
142
  async init() {
38
143
  await mkdir(this.dataDir, { recursive: true, mode: 448 });
@@ -149,7 +254,7 @@ class JsonEventsStore {
149
254
  };
150
255
  }
151
256
  async ensureArrayFile(path) {
152
- if (!existsSync(path)) {
257
+ if (!existsSync2(path)) {
153
258
  await writeFile(path, `[]
154
259
  `, { encoding: "utf-8", mode: 384 });
155
260
  }
@@ -179,7 +284,7 @@ class JsonEventsStore {
179
284
  });
180
285
  }
181
286
  }
182
- function localJsonRuntime(dataDir = getEventsDataDir()) {
287
+ function localJsonRuntime(dataDir2 = getEventsDataDir()) {
183
288
  return {
184
289
  mode: "local-files",
185
290
  name: "json-events-store",
@@ -192,7 +297,7 @@ function localJsonRuntime(dataDir = getEventsDataDir()) {
192
297
  durable: true,
193
298
  idempotency: "best-effort-local",
194
299
  replayCursors: true,
195
- description: `Local JSON files in ${dataDir}; no SQLite, Postgres, S3, or AWS runtime is configured by this store.`
300
+ description: `Local JSON files in ${dataDir2}; no SQLite, Postgres, S3, or AWS runtime is configured by this store.`
196
301
  };
197
302
  }
198
303
  function encodeLocalJsonEventCursor(offset, options = {}) {
@@ -256,8 +361,8 @@ function assertCursorFilter(name, cursorValue, optionValue) {
256
361
  function findEventByIdentity(events, identity) {
257
362
  return events.find((event) => identity.id !== undefined && event.id === identity.id || identity.dedupeKey !== undefined && event.dedupeKey === identity.dedupeKey);
258
363
  }
259
- async function getEventsStatus(dataDir) {
260
- const store = new JsonEventsStore(dataDir);
364
+ async function getEventsStatus(dataDir2) {
365
+ const store = new JsonEventsStore(dataDir2);
261
366
  await store.init();
262
367
  const [channels, events, deliveries] = await Promise.all([
263
368
  store.listChannels(),
@@ -299,22 +404,22 @@ async function getEventsStatus(dataDir) {
299
404
  }
300
405
  };
301
406
  }
302
- function statusFile(dataDir, fileName, records) {
303
- const path = join(dataDir, fileName);
304
- return { path, exists: existsSync(path), records };
407
+ function statusFile(dataDir2, fileName, records) {
408
+ const path = join3(dataDir2, fileName);
409
+ return { path, exists: existsSync2(path), records };
305
410
  }
306
411
  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,
412
+ normalizeEventPageLimit,
318
413
  localJsonRuntime,
319
- normalizeEventPageLimit
414
+ getEventsStatus,
415
+ getEventsDataDir,
416
+ getActiveEventsDirEnv,
417
+ encodeLocalJsonEventCursor,
418
+ decodeLocalJsonEventCursor,
419
+ MAX_EVENT_PAGE_LIMIT,
420
+ LOCAL_JSON_EVENT_CURSOR_PREFIX,
421
+ JsonEventsStore,
422
+ HASNA_EVENTS_HOME_ENV,
423
+ HASNA_EVENTS_DIR_ENV,
424
+ DEFAULT_EVENT_PAGE_LIMIT
320
425
  };