@hasna/events 0.1.17 → 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/README.md CHANGED
@@ -469,7 +469,10 @@ const events = new EventsClient({
469
469
 
470
470
  ## CLI
471
471
 
472
- The package exposes `events` and `hasna-events`.
472
+ The package exposes `events` and `hasna-events`. `hasna-events` is a
473
+ **deprecated alias** of `events` (same entrypoint, kept for npm parity with
474
+ 0.1.15); new usage should call `events` directly, and the alias is scheduled
475
+ for removal in a future major release (hasna/apps#1602).
473
476
 
474
477
  Global options must come before the command group:
475
478
 
@@ -491,6 +494,17 @@ events channels match ops
491
494
  events channels remove ops
492
495
  ```
493
496
 
497
+ `channels test` exits with code 1 when delivery fails and code 0 when delivery
498
+ succeeds or is skipped by `--honor-filters`. Its JSON delivery result and human
499
+ status output are preserved in either case.
500
+
501
+ The standalone CLI and default embedded Commander commands deny private webhook
502
+ targets unless the administrator sets `HASNA_EVENTS_ALLOW_PRIVATE_WEBHOOK_TARGETS`
503
+ to a comma-separated list of exact hostnames or IP addresses. For an intentional
504
+ local receiver, set `HASNA_EVENTS_ALLOW_PRIVATE_WEBHOOK_TARGETS=127.0.0.1`.
505
+ This setting does not change SDK defaults or override an embedded host's custom
506
+ `createClient` policy. Redirects and DNS results still undergo target validation.
507
+
494
508
  Field filters can match nested `data` or `metadata` values. Plain
495
509
  `--data`/`--metadata` values are strings, which keeps ids and slugs such as
496
510
  `001` intact. Use `--data-json` or `--metadata-json` for typed JSON predicates.
package/dist/cli/index.js CHANGED
@@ -3,7 +3,7 @@
3
3
 
4
4
  // src/cli/index.ts
5
5
  import { readFileSync as readFileSync2 } from "fs";
6
- import { dirname, join as join7 } from "path";
6
+ import { dirname, join as join6 } from "path";
7
7
  import { fileURLToPath } from "url";
8
8
 
9
9
  // src/index.ts
@@ -110,87 +110,80 @@ function channelMatchesEvent(channel, event) {
110
110
  import { chmod, mkdir, readFile, rename, writeFile } from "fs/promises";
111
111
  import { Buffer as Buffer2 } from "buffer";
112
112
  import { existsSync as existsSync2 } from "fs";
113
- import { join as join3 } from "path";
113
+ import { join as join2 } from "path";
114
114
 
115
115
  // src/app-home.ts
116
116
  import { existsSync } from "fs";
117
- import { homedir as homedir2 } from "os";
118
- import { join as join2, resolve } from "path";
119
-
120
- // ../../node_modules/.bun/@hasna+paths@0.1.0/node_modules/@hasna/paths/dist/index.js
121
117
  import { homedir } from "os";
122
- import { join } from "path";
123
- var KIND_ENV = {
118
+ import { join, resolve } from "path";
119
+ import { homedir as pathsResolverHomedir } from "os";
120
+ import { join as pathsResolverJoin } from "path";
121
+ var PATHS_RESOLVER_KIND_ENV = {
124
122
  config: "HASNA_CONFIG_HOME",
125
123
  data: "HASNA_DATA_HOME",
126
124
  state: "HASNA_STATE_HOME",
127
125
  cache: "HASNA_CACHE_HOME"
128
126
  };
129
- var APP_SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
130
- function assertApp(app) {
127
+ var PATHS_RESOLVER_APP_SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
128
+ function pathsResolverAssertApp(app) {
131
129
  if (typeof app !== "string" || app.length === 0) {
132
130
  throw new TypeError("paths: app must be a non-empty string");
133
131
  }
134
- if (!APP_SLUG_RE.test(app)) {
132
+ if (!PATHS_RESOLVER_APP_SLUG_RE.test(app)) {
135
133
  throw new TypeError(`paths: invalid app slug "${app}" \u2014 expected lowercase kebab-case ([a-z0-9]+(-[a-z0-9]+)*)`);
136
134
  }
137
135
  }
138
- function envOf(options) {
139
- return options.env ?? process.env;
140
- }
141
- function envValue(options, kind) {
142
- const value = envOf(options)[KIND_ENV[kind]];
143
- return typeof value === "string" && value.length > 0 ? value : undefined;
144
- }
145
- function isMacOS(platform) {
146
- return platform === "darwin";
136
+ function pathsResolverAssertKind(kind) {
137
+ if (!Object.keys(PATHS_RESOLVER_KIND_ENV).includes(kind)) {
138
+ throw new TypeError(`paths: invalid path kind "${kind}" \u2014 expected one of ${Object.keys(PATHS_RESOLVER_KIND_ENV).join(", ")}`);
139
+ }
147
140
  }
148
- function baseDir(kind, options) {
149
- const override = envValue(options, kind);
150
- if (override)
141
+ function pathsResolverBaseDir(kind, options) {
142
+ pathsResolverAssertKind(kind);
143
+ const env = options.env ?? process.env;
144
+ const override = env[PATHS_RESOLVER_KIND_ENV[kind]];
145
+ if (typeof override === "string" && override.length > 0)
151
146
  return override;
152
- const home = options.home ?? homedir();
147
+ const home = options.home ?? pathsResolverHomedir();
153
148
  const platform = options.platform ?? process.platform;
154
- if (isMacOS(platform)) {
149
+ if (platform === "darwin") {
155
150
  switch (kind) {
156
151
  case "config":
157
152
  case "data":
158
- return join(home, "Library", "Application Support", "Hasna");
153
+ return pathsResolverJoin(home, "Library", "Application Support", "Hasna");
159
154
  case "cache":
160
- return join(home, "Library", "Caches", "Hasna");
155
+ return pathsResolverJoin(home, "Library", "Caches", "Hasna");
161
156
  case "state":
162
- return join(home, "Library", "Logs", "Hasna");
157
+ return pathsResolverJoin(home, "Library", "Logs", "Hasna");
163
158
  }
164
159
  }
165
160
  switch (kind) {
166
161
  case "config":
167
- return join(home, ".config", "hasna");
162
+ return pathsResolverJoin(home, ".config", "hasna");
168
163
  case "data":
169
- return join(home, ".local", "share", "hasna");
164
+ return pathsResolverJoin(home, ".local", "share", "hasna");
170
165
  case "state":
171
- return join(home, ".local", "state", "hasna");
166
+ return pathsResolverJoin(home, ".local", "state", "hasna");
172
167
  case "cache":
173
- return join(home, ".cache", "hasna");
168
+ return pathsResolverJoin(home, ".cache", "hasna");
174
169
  }
175
170
  }
176
- function resolvePath(kind, options) {
177
- assertApp(options.app);
178
- const appSegment = options.internal === true ? join("internal", options.app) : options.app;
179
- return join(baseDir(kind, options), appSegment);
171
+ function pathsResolverResolve(kind, options) {
172
+ pathsResolverAssertApp(options.app);
173
+ const appSegment = options.internal === true ? pathsResolverJoin("internal", options.app) : options.app;
174
+ return pathsResolverJoin(pathsResolverBaseDir(kind, options), appSegment);
180
175
  }
181
176
  function dataDir(options) {
182
- return resolvePath("data", options);
177
+ return pathsResolverResolve("data", options);
183
178
  }
184
-
185
- // src/app-home.ts
186
179
  var HASNA_EVENTS_DIR_ENV = "HASNA_EVENTS_DIR";
187
180
  var HASNA_EVENTS_HOME_ENV = "HASNA_EVENTS_HOME";
188
181
  var EVENTS_STORE_SENTINEL_FILE = "events.json";
189
182
  function effectiveHome() {
190
- return process.env["HOME"] || process.env["USERPROFILE"] || homedir2();
183
+ return process.env["HOME"] || process.env["USERPROFILE"] || homedir();
191
184
  }
192
185
  function legacyHomeDir() {
193
- return join2(effectiveHome(), ".hasna", "events");
186
+ return join(effectiveHome(), ".hasna", "events");
194
187
  }
195
188
  function resolverHome() {
196
189
  return dataDir({ app: "events", home: effectiveHome() || undefined });
@@ -199,7 +192,7 @@ function adoptResolverHome(resolved, env = process.env) {
199
192
  const dataOverride = env.HASNA_DATA_HOME;
200
193
  if (typeof dataOverride === "string" && dataOverride.trim().length > 0)
201
194
  return true;
202
- return existsSync(join2(resolved, EVENTS_STORE_SENTINEL_FILE));
195
+ return existsSync(join(resolved, EVENTS_STORE_SENTINEL_FILE));
203
196
  }
204
197
  function exactEventsHome() {
205
198
  const dir = process.env[HASNA_EVENTS_DIR_ENV];
@@ -242,9 +235,9 @@ class JsonEventsStore {
242
235
  constructor(dataDir2 = getEventsDataDir()) {
243
236
  this.dataDir = dataDir2;
244
237
  this.runtime = localJsonRuntime(dataDir2);
245
- this.channelsPath = join3(dataDir2, "channels.json");
246
- this.eventsPath = join3(dataDir2, "events.json");
247
- this.deliveriesPath = join3(dataDir2, "deliveries.json");
238
+ this.channelsPath = join2(dataDir2, "channels.json");
239
+ this.eventsPath = join2(dataDir2, "events.json");
240
+ this.deliveriesPath = join2(dataDir2, "deliveries.json");
248
241
  }
249
242
  async init() {
250
243
  await mkdir(this.dataDir, { recursive: true, mode: 448 });
@@ -512,7 +505,7 @@ async function getEventsStatus(dataDir2) {
512
505
  };
513
506
  }
514
507
  function statusFile(dataDir2, fileName, records) {
515
- const path = join3(dataDir2, fileName);
508
+ const path = join2(dataDir2, fileName);
516
509
  return { path, exists: existsSync2(path), records };
517
510
  }
518
511
 
@@ -1414,7 +1407,7 @@ import {
1414
1407
  unlinkSync,
1415
1408
  writeFileSync
1416
1409
  } from "fs";
1417
- import { basename, join as join4 } from "path";
1410
+ import { basename, join as join3 } from "path";
1418
1411
  var DURABLE_SCHEMA_VERSION = 1;
1419
1412
  var MAX_RETRY_ATTEMPTS = 1000;
1420
1413
  var MAX_RETRY_DELAY_MS = 365 * 24 * 60 * 60 * 1000;
@@ -1557,7 +1550,7 @@ class DurableEventsBroker {
1557
1550
  if (!options.dataDir)
1558
1551
  throw new Error("DurableEventsBroker requires dataDir");
1559
1552
  this.dataDir = options.dataDir;
1560
- this.databasePath = join4(options.dataDir, options.databaseName ?? "events.sqlite");
1553
+ this.databasePath = join3(options.dataDir, options.databaseName ?? "events.sqlite");
1561
1554
  this.now = options.now ?? (() => new Date);
1562
1555
  this.transportOptions = {
1563
1556
  fetchImpl: options.fetchImpl,
@@ -1701,14 +1694,14 @@ class DurableEventsBroker {
1701
1694
  return summary;
1702
1695
  }
1703
1696
  importSpool(options = {}) {
1704
- const inboxDir = join4(this.dataDir, "spool", "inbox");
1697
+ const inboxDir = join3(this.dataDir, "spool", "inbox");
1705
1698
  if (!existsSync3(inboxDir))
1706
1699
  return { scanned: 0, imported: 0, deduped: 0, queued: 0, quarantined: 0 };
1707
1700
  const limit = normalizePositiveInteger(options.limit, 100, "limit");
1708
1701
  const names = readdirSync(inboxDir).filter((name) => /^[a-f0-9]{64}\.json$/.test(name)).sort().slice(0, limit);
1709
1702
  const result = { scanned: names.length, imported: 0, deduped: 0, queued: 0, quarantined: 0 };
1710
1703
  for (const name of names) {
1711
- const path = join4(inboxDir, name);
1704
+ const path = join3(inboxDir, name);
1712
1705
  let event;
1713
1706
  try {
1714
1707
  event = parseSpoolEnvelope(readFileSync(path, "utf8"));
@@ -2180,22 +2173,22 @@ function syncDirectory(path) {
2180
2173
  }
2181
2174
  }
2182
2175
  function quarantineSpoolRecord(dataDir2, path, reason) {
2183
- const spoolDir = join4(dataDir2, "spool");
2184
- const quarantineDir = join4(spoolDir, "quarantine");
2176
+ const spoolDir = join3(dataDir2, "spool");
2177
+ const quarantineDir = join3(spoolDir, "quarantine");
2185
2178
  mkdirSync(quarantineDir, { recursive: true, mode: 448 });
2186
2179
  chmodSync(spoolDir, 448);
2187
2180
  chmodSync(quarantineDir, 448);
2188
2181
  const name = basename(path);
2189
2182
  const base = name.replace(/\.json$/, "");
2190
2183
  const suffix = `${Date.now()}-${randomUUID3().slice(0, 8)}`;
2191
- const destination = join4(quarantineDir, `${base}.${suffix}.json`);
2184
+ const destination = join3(quarantineDir, `${base}.${suffix}.json`);
2192
2185
  renameSync(path, destination);
2193
2186
  const metadata = {
2194
2187
  quarantinedAt: new Date().toISOString(),
2195
2188
  originalName: name,
2196
2189
  reason
2197
2190
  };
2198
- writeFileSync(join4(quarantineDir, `${base}.${suffix}.meta.json`), `${JSON.stringify(metadata, null, 2)}
2191
+ writeFileSync(join3(quarantineDir, `${base}.${suffix}.meta.json`), `${JSON.stringify(metadata, null, 2)}
2199
2192
  `, { mode: 384 });
2200
2193
  syncDirectory(quarantineDir);
2201
2194
  }
@@ -2210,7 +2203,7 @@ function spoolFileName(event) {
2210
2203
  // src/durable-worker.ts
2211
2204
  import { chmodSync as chmodSync2, mkdirSync as mkdirSync2, watch } from "fs";
2212
2205
  import { randomUUID as randomUUID5 } from "crypto";
2213
- import { join as join6 } from "path";
2206
+ import { join as join5 } from "path";
2214
2207
 
2215
2208
  // src/durable-spool.ts
2216
2209
  import { createHash as createHash2, randomUUID as randomUUID4 } from "crypto";
@@ -2224,7 +2217,7 @@ import {
2224
2217
  stat,
2225
2218
  unlink
2226
2219
  } from "fs/promises";
2227
- import { join as join5 } from "path";
2220
+ import { join as join4 } from "path";
2228
2221
  class DurableEventSpool {
2229
2222
  dataDir;
2230
2223
  inboxDir;
@@ -2232,13 +2225,13 @@ class DurableEventSpool {
2232
2225
  if (!options.dataDir)
2233
2226
  throw new Error("DurableEventSpool requires dataDir");
2234
2227
  this.dataDir = options.dataDir;
2235
- this.inboxDir = join5(options.dataDir, "spool", "inbox");
2228
+ this.inboxDir = join4(options.dataDir, "spool", "inbox");
2236
2229
  }
2237
2230
  async enqueue(input) {
2238
2231
  const event = redactSensitiveKeys(createSpoolEvent(input));
2239
2232
  await this.ensureInbox();
2240
2233
  const finalPath = this.pathFor(event);
2241
- const tempPath = join5(this.inboxDir, `.tmp-${process.pid}-${randomUUID4()}`);
2234
+ const tempPath = join4(this.inboxDir, `.tmp-${process.pid}-${randomUUID4()}`);
2242
2235
  const handle = await open(tempPath, "wx", 384);
2243
2236
  try {
2244
2237
  await handle.writeFile(`${JSON.stringify(event)}
@@ -2271,7 +2264,7 @@ class DurableEventSpool {
2271
2264
  const result = { recovered: 0, deduped: 0, cleaned: 0 };
2272
2265
  const names = (await readdir(this.inboxDir)).filter((name) => name.startsWith(".tmp-")).sort();
2273
2266
  for (const name of names) {
2274
- const tempPath = join5(this.inboxDir, name);
2267
+ const tempPath = join4(this.inboxDir, name);
2275
2268
  const details = await stat(tempPath).catch(() => {
2276
2269
  return;
2277
2270
  });
@@ -2309,7 +2302,7 @@ class DurableEventSpool {
2309
2302
  pathFor(event) {
2310
2303
  const identity = event.dedupeKey ?? event.id;
2311
2304
  const digest = createHash2("sha256").update(identity, "utf8").digest("hex");
2312
- return join5(this.inboxDir, `${digest}.json`);
2305
+ return join4(this.inboxDir, `${digest}.json`);
2313
2306
  }
2314
2307
  async assertSameIdentity(path, event) {
2315
2308
  const existing = parseEnvelope(await readFile2(path, "utf8"));
@@ -2318,7 +2311,7 @@ class DurableEventSpool {
2318
2311
  throw new Error("Durable spool identity collision");
2319
2312
  }
2320
2313
  async ensureInbox() {
2321
- const spoolDir = join5(this.dataDir, "spool");
2314
+ const spoolDir = join4(this.dataDir, "spool");
2322
2315
  await mkdir2(this.inboxDir, { recursive: true, mode: 448 });
2323
2316
  await chmod2(this.dataDir, 448);
2324
2317
  await chmod2(spoolDir, 448);
@@ -2386,7 +2379,7 @@ async function runDurableWorker(options) {
2386
2379
  const spool = new DurableEventSpool({ dataDir: options.broker.dataDir });
2387
2380
  const inboxDir = spool.inboxDir;
2388
2381
  mkdirSync2(inboxDir, { recursive: true, mode: 448 });
2389
- chmodSync2(join6(options.broker.dataDir, "spool"), 448);
2382
+ chmodSync2(join5(options.broker.dataDir, "spool"), 448);
2390
2383
  chmodSync2(inboxDir, 448);
2391
2384
  const totals = {
2392
2385
  workerId,
@@ -2612,10 +2605,19 @@ function parseMatcherExpression(value, label) {
2612
2605
  };
2613
2606
  }
2614
2607
 
2608
+ // src/cli-webhook-policy.ts
2609
+ function webhookTargetPolicyFromEnv() {
2610
+ const value = process.env.HASNA_EVENTS_ALLOW_PRIVATE_WEBHOOK_TARGETS;
2611
+ if (!value)
2612
+ return;
2613
+ const hosts = value.split(",").map((entry) => entry.trim()).filter((entry) => entry.length > 0);
2614
+ return hosts.length > 0 ? { allowPrivateHosts: hosts } : undefined;
2615
+ }
2616
+
2615
2617
  // src/cli/index.ts
2616
2618
  function version() {
2617
2619
  try {
2618
- const packagePath = join7(dirname(fileURLToPath(import.meta.url)), "..", "..", "package.json");
2620
+ const packagePath = join6(dirname(fileURLToPath(import.meta.url)), "..", "..", "package.json");
2619
2621
  return JSON.parse(readFileSync2(packagePath, "utf-8")).version ?? "0.0.0";
2620
2622
  } catch {
2621
2623
  return "0.0.0";
@@ -3158,6 +3160,8 @@ async function handleChannels(client, command, tail, parsed, options) {
3158
3160
  metadata: parseJsonOption(takeOption(args, "--metadata"), {})
3159
3161
  }, { honorFilters });
3160
3162
  output(parsed, result, () => console.log(`${result.status}: ${result.channelId}`));
3163
+ if (result.status === "failed")
3164
+ process.exitCode = 1;
3161
3165
  return;
3162
3166
  }
3163
3167
  if (command === "match") {
@@ -3268,13 +3272,6 @@ function replaySummary(events, deliveries, nextCursor) {
3268
3272
  const suffix = nextCursor ? `, next cursor: ${nextCursor}` : "";
3269
3273
  return `Replayed ${events} event(s), ${deliveries} delivery result(s)${suffix}`;
3270
3274
  }
3271
- function webhookTargetPolicyFromEnv() {
3272
- const value = process.env.HASNA_EVENTS_ALLOW_PRIVATE_WEBHOOK_TARGETS;
3273
- if (!value)
3274
- return;
3275
- const hosts = value.split(",").map((entry) => entry.trim()).filter((entry) => entry.length > 0);
3276
- return hosts.length > 0 ? { allowPrivateHosts: hosts } : undefined;
3277
- }
3278
3275
  if (import.meta.main) {
3279
3276
  runEventsCli().catch((error) => {
3280
3277
  const parsed = parseGlobalArgs(process.argv.slice(2));
package/dist/commander.js CHANGED
@@ -100,87 +100,80 @@ function channelMatchesEvent(channel, event) {
100
100
  import { chmod, mkdir, readFile, rename, writeFile } from "fs/promises";
101
101
  import { Buffer as Buffer2 } from "buffer";
102
102
  import { existsSync as existsSync2 } from "fs";
103
- import { join as join3 } from "path";
103
+ import { join as join2 } from "path";
104
104
 
105
105
  // src/app-home.ts
106
106
  import { existsSync } from "fs";
107
- import { homedir as homedir2 } from "os";
108
- import { join as join2, resolve } from "path";
109
-
110
- // ../../node_modules/.bun/@hasna+paths@0.1.0/node_modules/@hasna/paths/dist/index.js
111
107
  import { homedir } from "os";
112
- import { join } from "path";
113
- var KIND_ENV = {
108
+ import { join, resolve } from "path";
109
+ import { homedir as pathsResolverHomedir } from "os";
110
+ import { join as pathsResolverJoin } from "path";
111
+ var PATHS_RESOLVER_KIND_ENV = {
114
112
  config: "HASNA_CONFIG_HOME",
115
113
  data: "HASNA_DATA_HOME",
116
114
  state: "HASNA_STATE_HOME",
117
115
  cache: "HASNA_CACHE_HOME"
118
116
  };
119
- var APP_SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
120
- function assertApp(app) {
117
+ var PATHS_RESOLVER_APP_SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
118
+ function pathsResolverAssertApp(app) {
121
119
  if (typeof app !== "string" || app.length === 0) {
122
120
  throw new TypeError("paths: app must be a non-empty string");
123
121
  }
124
- if (!APP_SLUG_RE.test(app)) {
122
+ if (!PATHS_RESOLVER_APP_SLUG_RE.test(app)) {
125
123
  throw new TypeError(`paths: invalid app slug "${app}" \u2014 expected lowercase kebab-case ([a-z0-9]+(-[a-z0-9]+)*)`);
126
124
  }
127
125
  }
128
- function envOf(options) {
129
- return options.env ?? process.env;
130
- }
131
- function envValue(options, kind) {
132
- const value = envOf(options)[KIND_ENV[kind]];
133
- return typeof value === "string" && value.length > 0 ? value : undefined;
134
- }
135
- function isMacOS(platform) {
136
- return platform === "darwin";
126
+ function pathsResolverAssertKind(kind) {
127
+ if (!Object.keys(PATHS_RESOLVER_KIND_ENV).includes(kind)) {
128
+ throw new TypeError(`paths: invalid path kind "${kind}" \u2014 expected one of ${Object.keys(PATHS_RESOLVER_KIND_ENV).join(", ")}`);
129
+ }
137
130
  }
138
- function baseDir(kind, options) {
139
- const override = envValue(options, kind);
140
- if (override)
131
+ function pathsResolverBaseDir(kind, options) {
132
+ pathsResolverAssertKind(kind);
133
+ const env = options.env ?? process.env;
134
+ const override = env[PATHS_RESOLVER_KIND_ENV[kind]];
135
+ if (typeof override === "string" && override.length > 0)
141
136
  return override;
142
- const home = options.home ?? homedir();
137
+ const home = options.home ?? pathsResolverHomedir();
143
138
  const platform = options.platform ?? process.platform;
144
- if (isMacOS(platform)) {
139
+ if (platform === "darwin") {
145
140
  switch (kind) {
146
141
  case "config":
147
142
  case "data":
148
- return join(home, "Library", "Application Support", "Hasna");
143
+ return pathsResolverJoin(home, "Library", "Application Support", "Hasna");
149
144
  case "cache":
150
- return join(home, "Library", "Caches", "Hasna");
145
+ return pathsResolverJoin(home, "Library", "Caches", "Hasna");
151
146
  case "state":
152
- return join(home, "Library", "Logs", "Hasna");
147
+ return pathsResolverJoin(home, "Library", "Logs", "Hasna");
153
148
  }
154
149
  }
155
150
  switch (kind) {
156
151
  case "config":
157
- return join(home, ".config", "hasna");
152
+ return pathsResolverJoin(home, ".config", "hasna");
158
153
  case "data":
159
- return join(home, ".local", "share", "hasna");
154
+ return pathsResolverJoin(home, ".local", "share", "hasna");
160
155
  case "state":
161
- return join(home, ".local", "state", "hasna");
156
+ return pathsResolverJoin(home, ".local", "state", "hasna");
162
157
  case "cache":
163
- return join(home, ".cache", "hasna");
158
+ return pathsResolverJoin(home, ".cache", "hasna");
164
159
  }
165
160
  }
166
- function resolvePath(kind, options) {
167
- assertApp(options.app);
168
- const appSegment = options.internal === true ? join("internal", options.app) : options.app;
169
- return join(baseDir(kind, options), appSegment);
161
+ function pathsResolverResolve(kind, options) {
162
+ pathsResolverAssertApp(options.app);
163
+ const appSegment = options.internal === true ? pathsResolverJoin("internal", options.app) : options.app;
164
+ return pathsResolverJoin(pathsResolverBaseDir(kind, options), appSegment);
170
165
  }
171
166
  function dataDir(options) {
172
- return resolvePath("data", options);
167
+ return pathsResolverResolve("data", options);
173
168
  }
174
-
175
- // src/app-home.ts
176
169
  var HASNA_EVENTS_DIR_ENV = "HASNA_EVENTS_DIR";
177
170
  var HASNA_EVENTS_HOME_ENV = "HASNA_EVENTS_HOME";
178
171
  var EVENTS_STORE_SENTINEL_FILE = "events.json";
179
172
  function effectiveHome() {
180
- return process.env["HOME"] || process.env["USERPROFILE"] || homedir2();
173
+ return process.env["HOME"] || process.env["USERPROFILE"] || homedir();
181
174
  }
182
175
  function legacyHomeDir() {
183
- return join2(effectiveHome(), ".hasna", "events");
176
+ return join(effectiveHome(), ".hasna", "events");
184
177
  }
185
178
  function resolverHome() {
186
179
  return dataDir({ app: "events", home: effectiveHome() || undefined });
@@ -189,7 +182,7 @@ function adoptResolverHome(resolved, env = process.env) {
189
182
  const dataOverride = env.HASNA_DATA_HOME;
190
183
  if (typeof dataOverride === "string" && dataOverride.trim().length > 0)
191
184
  return true;
192
- return existsSync(join2(resolved, EVENTS_STORE_SENTINEL_FILE));
185
+ return existsSync(join(resolved, EVENTS_STORE_SENTINEL_FILE));
193
186
  }
194
187
  function exactEventsHome() {
195
188
  const dir = process.env[HASNA_EVENTS_DIR_ENV];
@@ -232,9 +225,9 @@ class JsonEventsStore {
232
225
  constructor(dataDir2 = getEventsDataDir()) {
233
226
  this.dataDir = dataDir2;
234
227
  this.runtime = localJsonRuntime(dataDir2);
235
- this.channelsPath = join3(dataDir2, "channels.json");
236
- this.eventsPath = join3(dataDir2, "events.json");
237
- this.deliveriesPath = join3(dataDir2, "deliveries.json");
228
+ this.channelsPath = join2(dataDir2, "channels.json");
229
+ this.eventsPath = join2(dataDir2, "events.json");
230
+ this.deliveriesPath = join2(dataDir2, "deliveries.json");
238
231
  }
239
232
  async init() {
240
233
  await mkdir(this.dataDir, { recursive: true, mode: 448 });
@@ -502,7 +495,7 @@ async function getEventsStatus(dataDir2) {
502
495
  };
503
496
  }
504
497
  function statusFile(dataDir2, fileName, records) {
505
- const path = join3(dataDir2, fileName);
498
+ const path = join2(dataDir2, fileName);
506
499
  return { path, exists: existsSync2(path), records };
507
500
  }
508
501
 
@@ -1980,6 +1973,15 @@ function parseMatcherExpression(value, label) {
1980
1973
  };
1981
1974
  }
1982
1975
 
1976
+ // src/cli-webhook-policy.ts
1977
+ function webhookTargetPolicyFromEnv() {
1978
+ const value = process.env.HASNA_EVENTS_ALLOW_PRIVATE_WEBHOOK_TARGETS;
1979
+ if (!value)
1980
+ return;
1981
+ const hosts = value.split(",").map((entry) => entry.trim()).filter((entry) => entry.length > 0);
1982
+ return hosts.length > 0 ? { allowPrivateHosts: hosts } : undefined;
1983
+ }
1984
+
1983
1985
  // src/commander.ts
1984
1986
  var DEFAULT_EVENT_LIST_LIMIT = 100;
1985
1987
  function parseJsonObject(value, fallback) {
@@ -2006,7 +2008,7 @@ function parseHeaders(values) {
2006
2008
  function createClient(options) {
2007
2009
  if (options.createClient)
2008
2010
  return options.createClient();
2009
- return new EventsClient({ store: new JsonEventsStore(options.dataDir) });
2011
+ return new EventsClient({ store: new JsonEventsStore(options.dataDir), webhookTargetPolicy: webhookTargetPolicyFromEnv() });
2010
2012
  }
2011
2013
  function print(value, json, text) {
2012
2014
  if (json)
@@ -2087,6 +2089,8 @@ function registerChannelCommands(program, options) {
2087
2089
  metadata: parseJsonObject(actionOptions.metadata, {})
2088
2090
  }, { honorFilters: actionOptions.honorFilters });
2089
2091
  print(result, json, `${result.status}: ${result.channelId}`);
2092
+ if (result.status === "failed")
2093
+ process.exitCode = 1;
2090
2094
  } catch (error) {
2091
2095
  fail(error, json);
2092
2096
  }
package/dist/durable.js CHANGED
@@ -100,87 +100,80 @@ function channelMatchesEvent(channel, event) {
100
100
  import { chmod, mkdir, readFile, rename, writeFile } from "fs/promises";
101
101
  import { Buffer as Buffer2 } from "buffer";
102
102
  import { existsSync as existsSync2 } from "fs";
103
- import { join as join3 } from "path";
103
+ import { join as join2 } from "path";
104
104
 
105
105
  // src/app-home.ts
106
106
  import { existsSync } from "fs";
107
- import { homedir as homedir2 } from "os";
108
- import { join as join2, resolve } from "path";
109
-
110
- // ../../node_modules/.bun/@hasna+paths@0.1.0/node_modules/@hasna/paths/dist/index.js
111
107
  import { homedir } from "os";
112
- import { join } from "path";
113
- var KIND_ENV = {
108
+ import { join, resolve } from "path";
109
+ import { homedir as pathsResolverHomedir } from "os";
110
+ import { join as pathsResolverJoin } from "path";
111
+ var PATHS_RESOLVER_KIND_ENV = {
114
112
  config: "HASNA_CONFIG_HOME",
115
113
  data: "HASNA_DATA_HOME",
116
114
  state: "HASNA_STATE_HOME",
117
115
  cache: "HASNA_CACHE_HOME"
118
116
  };
119
- var APP_SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
120
- function assertApp(app) {
117
+ var PATHS_RESOLVER_APP_SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
118
+ function pathsResolverAssertApp(app) {
121
119
  if (typeof app !== "string" || app.length === 0) {
122
120
  throw new TypeError("paths: app must be a non-empty string");
123
121
  }
124
- if (!APP_SLUG_RE.test(app)) {
122
+ if (!PATHS_RESOLVER_APP_SLUG_RE.test(app)) {
125
123
  throw new TypeError(`paths: invalid app slug "${app}" \u2014 expected lowercase kebab-case ([a-z0-9]+(-[a-z0-9]+)*)`);
126
124
  }
127
125
  }
128
- function envOf(options) {
129
- return options.env ?? process.env;
130
- }
131
- function envValue(options, kind) {
132
- const value = envOf(options)[KIND_ENV[kind]];
133
- return typeof value === "string" && value.length > 0 ? value : undefined;
134
- }
135
- function isMacOS(platform) {
136
- return platform === "darwin";
126
+ function pathsResolverAssertKind(kind) {
127
+ if (!Object.keys(PATHS_RESOLVER_KIND_ENV).includes(kind)) {
128
+ throw new TypeError(`paths: invalid path kind "${kind}" \u2014 expected one of ${Object.keys(PATHS_RESOLVER_KIND_ENV).join(", ")}`);
129
+ }
137
130
  }
138
- function baseDir(kind, options) {
139
- const override = envValue(options, kind);
140
- if (override)
131
+ function pathsResolverBaseDir(kind, options) {
132
+ pathsResolverAssertKind(kind);
133
+ const env = options.env ?? process.env;
134
+ const override = env[PATHS_RESOLVER_KIND_ENV[kind]];
135
+ if (typeof override === "string" && override.length > 0)
141
136
  return override;
142
- const home = options.home ?? homedir();
137
+ const home = options.home ?? pathsResolverHomedir();
143
138
  const platform = options.platform ?? process.platform;
144
- if (isMacOS(platform)) {
139
+ if (platform === "darwin") {
145
140
  switch (kind) {
146
141
  case "config":
147
142
  case "data":
148
- return join(home, "Library", "Application Support", "Hasna");
143
+ return pathsResolverJoin(home, "Library", "Application Support", "Hasna");
149
144
  case "cache":
150
- return join(home, "Library", "Caches", "Hasna");
145
+ return pathsResolverJoin(home, "Library", "Caches", "Hasna");
151
146
  case "state":
152
- return join(home, "Library", "Logs", "Hasna");
147
+ return pathsResolverJoin(home, "Library", "Logs", "Hasna");
153
148
  }
154
149
  }
155
150
  switch (kind) {
156
151
  case "config":
157
- return join(home, ".config", "hasna");
152
+ return pathsResolverJoin(home, ".config", "hasna");
158
153
  case "data":
159
- return join(home, ".local", "share", "hasna");
154
+ return pathsResolverJoin(home, ".local", "share", "hasna");
160
155
  case "state":
161
- return join(home, ".local", "state", "hasna");
156
+ return pathsResolverJoin(home, ".local", "state", "hasna");
162
157
  case "cache":
163
- return join(home, ".cache", "hasna");
158
+ return pathsResolverJoin(home, ".cache", "hasna");
164
159
  }
165
160
  }
166
- function resolvePath(kind, options) {
167
- assertApp(options.app);
168
- const appSegment = options.internal === true ? join("internal", options.app) : options.app;
169
- return join(baseDir(kind, options), appSegment);
161
+ function pathsResolverResolve(kind, options) {
162
+ pathsResolverAssertApp(options.app);
163
+ const appSegment = options.internal === true ? pathsResolverJoin("internal", options.app) : options.app;
164
+ return pathsResolverJoin(pathsResolverBaseDir(kind, options), appSegment);
170
165
  }
171
166
  function dataDir(options) {
172
- return resolvePath("data", options);
167
+ return pathsResolverResolve("data", options);
173
168
  }
174
-
175
- // src/app-home.ts
176
169
  var HASNA_EVENTS_DIR_ENV = "HASNA_EVENTS_DIR";
177
170
  var HASNA_EVENTS_HOME_ENV = "HASNA_EVENTS_HOME";
178
171
  var EVENTS_STORE_SENTINEL_FILE = "events.json";
179
172
  function effectiveHome() {
180
- return process.env["HOME"] || process.env["USERPROFILE"] || homedir2();
173
+ return process.env["HOME"] || process.env["USERPROFILE"] || homedir();
181
174
  }
182
175
  function legacyHomeDir() {
183
- return join2(effectiveHome(), ".hasna", "events");
176
+ return join(effectiveHome(), ".hasna", "events");
184
177
  }
185
178
  function resolverHome() {
186
179
  return dataDir({ app: "events", home: effectiveHome() || undefined });
@@ -189,7 +182,7 @@ function adoptResolverHome(resolved, env = process.env) {
189
182
  const dataOverride = env.HASNA_DATA_HOME;
190
183
  if (typeof dataOverride === "string" && dataOverride.trim().length > 0)
191
184
  return true;
192
- return existsSync(join2(resolved, EVENTS_STORE_SENTINEL_FILE));
185
+ return existsSync(join(resolved, EVENTS_STORE_SENTINEL_FILE));
193
186
  }
194
187
  function exactEventsHome() {
195
188
  const dir = process.env[HASNA_EVENTS_DIR_ENV];
@@ -232,9 +225,9 @@ class JsonEventsStore {
232
225
  constructor(dataDir2 = getEventsDataDir()) {
233
226
  this.dataDir = dataDir2;
234
227
  this.runtime = localJsonRuntime(dataDir2);
235
- this.channelsPath = join3(dataDir2, "channels.json");
236
- this.eventsPath = join3(dataDir2, "events.json");
237
- this.deliveriesPath = join3(dataDir2, "deliveries.json");
228
+ this.channelsPath = join2(dataDir2, "channels.json");
229
+ this.eventsPath = join2(dataDir2, "events.json");
230
+ this.deliveriesPath = join2(dataDir2, "deliveries.json");
238
231
  }
239
232
  async init() {
240
233
  await mkdir(this.dataDir, { recursive: true, mode: 448 });
@@ -502,7 +495,7 @@ async function getEventsStatus(dataDir2) {
502
495
  };
503
496
  }
504
497
  function statusFile(dataDir2, fileName, records) {
505
- const path = join3(dataDir2, fileName);
498
+ const path = join2(dataDir2, fileName);
506
499
  return { path, exists: existsSync2(path), records };
507
500
  }
508
501
 
@@ -1924,7 +1917,7 @@ import {
1924
1917
  unlinkSync,
1925
1918
  writeFileSync
1926
1919
  } from "fs";
1927
- import { basename, join as join4 } from "path";
1920
+ import { basename, join as join3 } from "path";
1928
1921
  var DURABLE_SCHEMA_VERSION = 1;
1929
1922
  var MAX_RETRY_ATTEMPTS = 1000;
1930
1923
  var MAX_RETRY_DELAY_MS = 365 * 24 * 60 * 60 * 1000;
@@ -2067,7 +2060,7 @@ class DurableEventsBroker {
2067
2060
  if (!options.dataDir)
2068
2061
  throw new Error("DurableEventsBroker requires dataDir");
2069
2062
  this.dataDir = options.dataDir;
2070
- this.databasePath = join4(options.dataDir, options.databaseName ?? "events.sqlite");
2063
+ this.databasePath = join3(options.dataDir, options.databaseName ?? "events.sqlite");
2071
2064
  this.now = options.now ?? (() => new Date);
2072
2065
  this.transportOptions = {
2073
2066
  fetchImpl: options.fetchImpl,
@@ -2211,14 +2204,14 @@ class DurableEventsBroker {
2211
2204
  return summary;
2212
2205
  }
2213
2206
  importSpool(options = {}) {
2214
- const inboxDir = join4(this.dataDir, "spool", "inbox");
2207
+ const inboxDir = join3(this.dataDir, "spool", "inbox");
2215
2208
  if (!existsSync3(inboxDir))
2216
2209
  return { scanned: 0, imported: 0, deduped: 0, queued: 0, quarantined: 0 };
2217
2210
  const limit = normalizePositiveInteger(options.limit, 100, "limit");
2218
2211
  const names = readdirSync(inboxDir).filter((name) => /^[a-f0-9]{64}\.json$/.test(name)).sort().slice(0, limit);
2219
2212
  const result = { scanned: names.length, imported: 0, deduped: 0, queued: 0, quarantined: 0 };
2220
2213
  for (const name of names) {
2221
- const path = join4(inboxDir, name);
2214
+ const path = join3(inboxDir, name);
2222
2215
  let event;
2223
2216
  try {
2224
2217
  event = parseSpoolEnvelope(readFileSync(path, "utf8"));
@@ -2690,22 +2683,22 @@ function syncDirectory(path) {
2690
2683
  }
2691
2684
  }
2692
2685
  function quarantineSpoolRecord(dataDir2, path, reason) {
2693
- const spoolDir = join4(dataDir2, "spool");
2694
- const quarantineDir = join4(spoolDir, "quarantine");
2686
+ const spoolDir = join3(dataDir2, "spool");
2687
+ const quarantineDir = join3(spoolDir, "quarantine");
2695
2688
  mkdirSync(quarantineDir, { recursive: true, mode: 448 });
2696
2689
  chmodSync(spoolDir, 448);
2697
2690
  chmodSync(quarantineDir, 448);
2698
2691
  const name = basename(path);
2699
2692
  const base = name.replace(/\.json$/, "");
2700
2693
  const suffix = `${Date.now()}-${randomUUID3().slice(0, 8)}`;
2701
- const destination = join4(quarantineDir, `${base}.${suffix}.json`);
2694
+ const destination = join3(quarantineDir, `${base}.${suffix}.json`);
2702
2695
  renameSync(path, destination);
2703
2696
  const metadata = {
2704
2697
  quarantinedAt: new Date().toISOString(),
2705
2698
  originalName: name,
2706
2699
  reason
2707
2700
  };
2708
- writeFileSync(join4(quarantineDir, `${base}.${suffix}.meta.json`), `${JSON.stringify(metadata, null, 2)}
2701
+ writeFileSync(join3(quarantineDir, `${base}.${suffix}.meta.json`), `${JSON.stringify(metadata, null, 2)}
2709
2702
  `, { mode: 384 });
2710
2703
  syncDirectory(quarantineDir);
2711
2704
  }
package/dist/index.js CHANGED
@@ -100,87 +100,80 @@ function channelMatchesEvent(channel, event) {
100
100
  import { chmod, mkdir, readFile, rename, writeFile } from "fs/promises";
101
101
  import { Buffer as Buffer2 } from "buffer";
102
102
  import { existsSync as existsSync2 } from "fs";
103
- import { join as join3 } from "path";
103
+ import { join as join2 } from "path";
104
104
 
105
105
  // src/app-home.ts
106
106
  import { existsSync } from "fs";
107
- import { homedir as homedir2 } from "os";
108
- import { join as join2, resolve } from "path";
109
-
110
- // ../../node_modules/.bun/@hasna+paths@0.1.0/node_modules/@hasna/paths/dist/index.js
111
107
  import { homedir } from "os";
112
- import { join } from "path";
113
- var KIND_ENV = {
108
+ import { join, resolve } from "path";
109
+ import { homedir as pathsResolverHomedir } from "os";
110
+ import { join as pathsResolverJoin } from "path";
111
+ var PATHS_RESOLVER_KIND_ENV = {
114
112
  config: "HASNA_CONFIG_HOME",
115
113
  data: "HASNA_DATA_HOME",
116
114
  state: "HASNA_STATE_HOME",
117
115
  cache: "HASNA_CACHE_HOME"
118
116
  };
119
- var APP_SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
120
- function assertApp(app) {
117
+ var PATHS_RESOLVER_APP_SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
118
+ function pathsResolverAssertApp(app) {
121
119
  if (typeof app !== "string" || app.length === 0) {
122
120
  throw new TypeError("paths: app must be a non-empty string");
123
121
  }
124
- if (!APP_SLUG_RE.test(app)) {
122
+ if (!PATHS_RESOLVER_APP_SLUG_RE.test(app)) {
125
123
  throw new TypeError(`paths: invalid app slug "${app}" \u2014 expected lowercase kebab-case ([a-z0-9]+(-[a-z0-9]+)*)`);
126
124
  }
127
125
  }
128
- function envOf(options) {
129
- return options.env ?? process.env;
130
- }
131
- function envValue(options, kind) {
132
- const value = envOf(options)[KIND_ENV[kind]];
133
- return typeof value === "string" && value.length > 0 ? value : undefined;
134
- }
135
- function isMacOS(platform) {
136
- return platform === "darwin";
126
+ function pathsResolverAssertKind(kind) {
127
+ if (!Object.keys(PATHS_RESOLVER_KIND_ENV).includes(kind)) {
128
+ throw new TypeError(`paths: invalid path kind "${kind}" \u2014 expected one of ${Object.keys(PATHS_RESOLVER_KIND_ENV).join(", ")}`);
129
+ }
137
130
  }
138
- function baseDir(kind, options) {
139
- const override = envValue(options, kind);
140
- if (override)
131
+ function pathsResolverBaseDir(kind, options) {
132
+ pathsResolverAssertKind(kind);
133
+ const env = options.env ?? process.env;
134
+ const override = env[PATHS_RESOLVER_KIND_ENV[kind]];
135
+ if (typeof override === "string" && override.length > 0)
141
136
  return override;
142
- const home = options.home ?? homedir();
137
+ const home = options.home ?? pathsResolverHomedir();
143
138
  const platform = options.platform ?? process.platform;
144
- if (isMacOS(platform)) {
139
+ if (platform === "darwin") {
145
140
  switch (kind) {
146
141
  case "config":
147
142
  case "data":
148
- return join(home, "Library", "Application Support", "Hasna");
143
+ return pathsResolverJoin(home, "Library", "Application Support", "Hasna");
149
144
  case "cache":
150
- return join(home, "Library", "Caches", "Hasna");
145
+ return pathsResolverJoin(home, "Library", "Caches", "Hasna");
151
146
  case "state":
152
- return join(home, "Library", "Logs", "Hasna");
147
+ return pathsResolverJoin(home, "Library", "Logs", "Hasna");
153
148
  }
154
149
  }
155
150
  switch (kind) {
156
151
  case "config":
157
- return join(home, ".config", "hasna");
152
+ return pathsResolverJoin(home, ".config", "hasna");
158
153
  case "data":
159
- return join(home, ".local", "share", "hasna");
154
+ return pathsResolverJoin(home, ".local", "share", "hasna");
160
155
  case "state":
161
- return join(home, ".local", "state", "hasna");
156
+ return pathsResolverJoin(home, ".local", "state", "hasna");
162
157
  case "cache":
163
- return join(home, ".cache", "hasna");
158
+ return pathsResolverJoin(home, ".cache", "hasna");
164
159
  }
165
160
  }
166
- function resolvePath(kind, options) {
167
- assertApp(options.app);
168
- const appSegment = options.internal === true ? join("internal", options.app) : options.app;
169
- return join(baseDir(kind, options), appSegment);
161
+ function pathsResolverResolve(kind, options) {
162
+ pathsResolverAssertApp(options.app);
163
+ const appSegment = options.internal === true ? pathsResolverJoin("internal", options.app) : options.app;
164
+ return pathsResolverJoin(pathsResolverBaseDir(kind, options), appSegment);
170
165
  }
171
166
  function dataDir(options) {
172
- return resolvePath("data", options);
167
+ return pathsResolverResolve("data", options);
173
168
  }
174
-
175
- // src/app-home.ts
176
169
  var HASNA_EVENTS_DIR_ENV = "HASNA_EVENTS_DIR";
177
170
  var HASNA_EVENTS_HOME_ENV = "HASNA_EVENTS_HOME";
178
171
  var EVENTS_STORE_SENTINEL_FILE = "events.json";
179
172
  function effectiveHome() {
180
- return process.env["HOME"] || process.env["USERPROFILE"] || homedir2();
173
+ return process.env["HOME"] || process.env["USERPROFILE"] || homedir();
181
174
  }
182
175
  function legacyHomeDir() {
183
- return join2(effectiveHome(), ".hasna", "events");
176
+ return join(effectiveHome(), ".hasna", "events");
184
177
  }
185
178
  function resolverHome() {
186
179
  return dataDir({ app: "events", home: effectiveHome() || undefined });
@@ -189,7 +182,7 @@ function adoptResolverHome(resolved, env = process.env) {
189
182
  const dataOverride = env.HASNA_DATA_HOME;
190
183
  if (typeof dataOverride === "string" && dataOverride.trim().length > 0)
191
184
  return true;
192
- return existsSync(join2(resolved, EVENTS_STORE_SENTINEL_FILE));
185
+ return existsSync(join(resolved, EVENTS_STORE_SENTINEL_FILE));
193
186
  }
194
187
  function exactEventsHome() {
195
188
  const dir = process.env[HASNA_EVENTS_DIR_ENV];
@@ -232,9 +225,9 @@ class JsonEventsStore {
232
225
  constructor(dataDir2 = getEventsDataDir()) {
233
226
  this.dataDir = dataDir2;
234
227
  this.runtime = localJsonRuntime(dataDir2);
235
- this.channelsPath = join3(dataDir2, "channels.json");
236
- this.eventsPath = join3(dataDir2, "events.json");
237
- this.deliveriesPath = join3(dataDir2, "deliveries.json");
228
+ this.channelsPath = join2(dataDir2, "channels.json");
229
+ this.eventsPath = join2(dataDir2, "events.json");
230
+ this.deliveriesPath = join2(dataDir2, "deliveries.json");
238
231
  }
239
232
  async init() {
240
233
  await mkdir(this.dataDir, { recursive: true, mode: 448 });
@@ -502,7 +495,7 @@ async function getEventsStatus(dataDir2) {
502
495
  };
503
496
  }
504
497
  function statusFile(dataDir2, fileName, records) {
505
- const path = join3(dataDir2, fileName);
498
+ const path = join2(dataDir2, fileName);
506
499
  return { path, exists: existsSync2(path), records };
507
500
  }
508
501
 
package/dist/storage.js CHANGED
@@ -3,87 +3,80 @@
3
3
  import { chmod, mkdir, readFile, rename, writeFile } from "fs/promises";
4
4
  import { Buffer } from "buffer";
5
5
  import { existsSync as existsSync2 } from "fs";
6
- import { join as join3 } from "path";
6
+ import { join as join2 } from "path";
7
7
 
8
8
  // src/app-home.ts
9
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
14
10
  import { homedir } from "os";
15
- import { join } from "path";
16
- var KIND_ENV = {
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 = {
17
15
  config: "HASNA_CONFIG_HOME",
18
16
  data: "HASNA_DATA_HOME",
19
17
  state: "HASNA_STATE_HOME",
20
18
  cache: "HASNA_CACHE_HOME"
21
19
  };
22
- var APP_SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
23
- function assertApp(app) {
20
+ var PATHS_RESOLVER_APP_SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
21
+ function pathsResolverAssertApp(app) {
24
22
  if (typeof app !== "string" || app.length === 0) {
25
23
  throw new TypeError("paths: app must be a non-empty string");
26
24
  }
27
- if (!APP_SLUG_RE.test(app)) {
25
+ if (!PATHS_RESOLVER_APP_SLUG_RE.test(app)) {
28
26
  throw new TypeError(`paths: invalid app slug "${app}" \u2014 expected lowercase kebab-case ([a-z0-9]+(-[a-z0-9]+)*)`);
29
27
  }
30
28
  }
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";
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
+ }
40
33
  }
41
- function baseDir(kind, options) {
42
- const override = envValue(options, kind);
43
- if (override)
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)
44
39
  return override;
45
- const home = options.home ?? homedir();
40
+ const home = options.home ?? pathsResolverHomedir();
46
41
  const platform = options.platform ?? process.platform;
47
- if (isMacOS(platform)) {
42
+ if (platform === "darwin") {
48
43
  switch (kind) {
49
44
  case "config":
50
45
  case "data":
51
- return join(home, "Library", "Application Support", "Hasna");
46
+ return pathsResolverJoin(home, "Library", "Application Support", "Hasna");
52
47
  case "cache":
53
- return join(home, "Library", "Caches", "Hasna");
48
+ return pathsResolverJoin(home, "Library", "Caches", "Hasna");
54
49
  case "state":
55
- return join(home, "Library", "Logs", "Hasna");
50
+ return pathsResolverJoin(home, "Library", "Logs", "Hasna");
56
51
  }
57
52
  }
58
53
  switch (kind) {
59
54
  case "config":
60
- return join(home, ".config", "hasna");
55
+ return pathsResolverJoin(home, ".config", "hasna");
61
56
  case "data":
62
- return join(home, ".local", "share", "hasna");
57
+ return pathsResolverJoin(home, ".local", "share", "hasna");
63
58
  case "state":
64
- return join(home, ".local", "state", "hasna");
59
+ return pathsResolverJoin(home, ".local", "state", "hasna");
65
60
  case "cache":
66
- return join(home, ".cache", "hasna");
61
+ return pathsResolverJoin(home, ".cache", "hasna");
67
62
  }
68
63
  }
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);
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);
73
68
  }
74
69
  function dataDir(options) {
75
- return resolvePath("data", options);
70
+ return pathsResolverResolve("data", options);
76
71
  }
77
-
78
- // src/app-home.ts
79
72
  var HASNA_EVENTS_DIR_ENV = "HASNA_EVENTS_DIR";
80
73
  var HASNA_EVENTS_HOME_ENV = "HASNA_EVENTS_HOME";
81
74
  var EVENTS_STORE_SENTINEL_FILE = "events.json";
82
75
  function effectiveHome() {
83
- return process.env["HOME"] || process.env["USERPROFILE"] || homedir2();
76
+ return process.env["HOME"] || process.env["USERPROFILE"] || homedir();
84
77
  }
85
78
  function legacyHomeDir() {
86
- return join2(effectiveHome(), ".hasna", "events");
79
+ return join(effectiveHome(), ".hasna", "events");
87
80
  }
88
81
  function resolverHome() {
89
82
  return dataDir({ app: "events", home: effectiveHome() || undefined });
@@ -92,7 +85,7 @@ function adoptResolverHome(resolved, env = process.env) {
92
85
  const dataOverride = env.HASNA_DATA_HOME;
93
86
  if (typeof dataOverride === "string" && dataOverride.trim().length > 0)
94
87
  return true;
95
- return existsSync(join2(resolved, EVENTS_STORE_SENTINEL_FILE));
88
+ return existsSync(join(resolved, EVENTS_STORE_SENTINEL_FILE));
96
89
  }
97
90
  function exactEventsHome() {
98
91
  const dir = process.env[HASNA_EVENTS_DIR_ENV];
@@ -135,9 +128,9 @@ class JsonEventsStore {
135
128
  constructor(dataDir2 = getEventsDataDir()) {
136
129
  this.dataDir = dataDir2;
137
130
  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");
131
+ this.channelsPath = join2(dataDir2, "channels.json");
132
+ this.eventsPath = join2(dataDir2, "events.json");
133
+ this.deliveriesPath = join2(dataDir2, "deliveries.json");
141
134
  }
142
135
  async init() {
143
136
  await mkdir(this.dataDir, { recursive: true, mode: 448 });
@@ -405,7 +398,7 @@ async function getEventsStatus(dataDir2) {
405
398
  };
406
399
  }
407
400
  function statusFile(dataDir2, fileName, records) {
408
- const path = join3(dataDir2, fileName);
401
+ const path = join2(dataDir2, fileName);
409
402
  return { path, exists: existsSync2(path), records };
410
403
  }
411
404
  export {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/events",
3
- "version": "0.1.17",
3
+ "version": "0.1.18",
4
4
  "description": "Shared event envelopes, local channels, replay, and delivery transports for Hasna open-source apps",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -113,12 +113,11 @@
113
113
  "author": "Hasna",
114
114
  "license": "Apache-2.0",
115
115
  "dependencies": {
116
- "@hasna/paths": "0.1.0",
117
116
  "commander": "^13.1.0"
118
117
  },
119
118
  "devDependencies": {
120
119
  "@hasna/contracts": "0.8.5",
121
- "@types/bun": "latest",
120
+ "@types/bun": "1.3.14",
122
121
  "typescript": "^5.7.3"
123
122
  }
124
123
  }
@@ -1,3 +1,12 @@
1
+ export type PathKind = "config" | "data" | "state" | "cache";
2
+ export interface PathsResolverOptions {
3
+ app: string;
4
+ internal?: boolean;
5
+ platform?: string;
6
+ home?: string;
7
+ env?: Record<string, string | undefined>;
8
+ }
9
+ export declare function dataDir(options: PathsResolverOptions): string;
1
10
  /** Env var names for the exact-app data-home overrides (preserved, highest precedence). */
2
11
  export declare const HASNA_EVENTS_DIR_ENV = "HASNA_EVENTS_DIR";
3
12
  export declare const HASNA_EVENTS_HOME_ENV = "HASNA_EVENTS_HOME";
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Shared CLI-only administrator opt-in for intentional private webhook ingress.
3
+ * The transport still requires an exact hostname/IP allowlist match and pins
4
+ * the validated address. SDK clients do not read this setting automatically.
5
+ */
6
+ export declare function webhookTargetPolicyFromEnv(): {
7
+ allowPrivateHosts: string[];
8
+ } | undefined;