@hasna/todos 0.15.46 → 0.15.47

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/mcp/index.js CHANGED
@@ -11084,8 +11084,9 @@ var init_event_hooks = __esm(() => {
11084
11084
  VALID_TARGETS = new Set(["stdout", "file", "socket", "script"]);
11085
11085
  });
11086
11086
 
11087
- // node_modules/.bun/@hasna+events@0.1.11/node_modules/@hasna/events/dist/index.js
11087
+ // node_modules/.bun/@hasna+events@0.1.16/node_modules/@hasna/events/dist/index.js
11088
11088
  import { chmod, mkdir, readFile, rename, writeFile } from "fs/promises";
11089
+ import { Buffer as Buffer2 } from "buffer";
11089
11090
  import { existsSync as existsSync5 } from "fs";
11090
11091
  import { homedir as homedir2 } from "os";
11091
11092
  import { join as join4 } from "path";
@@ -11194,11 +11195,13 @@ function getEventsDataDir(override) {
11194
11195
 
11195
11196
  class JsonEventsStore {
11196
11197
  dataDir;
11198
+ runtime;
11197
11199
  channelsPath;
11198
11200
  eventsPath;
11199
11201
  deliveriesPath;
11200
11202
  constructor(dataDir = getEventsDataDir()) {
11201
11203
  this.dataDir = dataDir;
11204
+ this.runtime = localJsonRuntime(dataDir);
11202
11205
  this.channelsPath = join4(dataDir, "channels.json");
11203
11206
  this.eventsPath = join4(dataDir, "events.json");
11204
11207
  this.deliveriesPath = join4(dataDir, "deliveries.json");
@@ -11246,13 +11249,58 @@ class JsonEventsStore {
11246
11249
  await this.writeJson(this.eventsPath, events);
11247
11250
  return event;
11248
11251
  }
11249
- async listEvents() {
11252
+ async appendEventOnce(event, options = {}) {
11250
11253
  await this.init();
11251
- return this.readJson(this.eventsPath, []);
11254
+ const events = await this.readJson(this.eventsPath, []);
11255
+ const dedupe = options.dedupe !== false;
11256
+ if (dedupe) {
11257
+ const existing = findEventByIdentity(events, { id: event.id, dedupeKey: event.dedupeKey });
11258
+ if (existing) {
11259
+ return {
11260
+ event: existing,
11261
+ stored: false,
11262
+ deduped: true,
11263
+ identity: { id: existing.id, dedupeKey: existing.dedupeKey }
11264
+ };
11265
+ }
11266
+ }
11267
+ events.push(event);
11268
+ await this.writeJson(this.eventsPath, events);
11269
+ return {
11270
+ event,
11271
+ stored: true,
11272
+ deduped: false,
11273
+ identity: { id: event.id, dedupeKey: event.dedupeKey }
11274
+ };
11275
+ }
11276
+ async listEvents(options = {}) {
11277
+ await this.init();
11278
+ const events = await this.readJson(this.eventsPath, []);
11279
+ return queryEvents(events, options);
11280
+ }
11281
+ async listEventsPage(options = {}) {
11282
+ await this.init();
11283
+ const events = await this.readJson(this.eventsPath, []);
11284
+ const queried = queryEvents(events, {
11285
+ eventId: options.eventId,
11286
+ source: options.source,
11287
+ type: options.type
11288
+ });
11289
+ const offset = decodeLocalJsonEventCursor(options.cursor, options);
11290
+ const limit = normalizeEventPageLimit(options.limit);
11291
+ const pageEvents = queried.slice(offset, offset + limit);
11292
+ const nextOffset = offset + pageEvents.length;
11293
+ const hasMore = nextOffset < queried.length;
11294
+ return {
11295
+ events: pageEvents,
11296
+ cursor: options.cursor,
11297
+ nextCursor: hasMore ? encodeLocalJsonEventCursor(nextOffset, options) : undefined,
11298
+ hasMore
11299
+ };
11252
11300
  }
11253
11301
  async findEventByIdentity(identity) {
11254
11302
  const events = await this.listEvents();
11255
- return events.find((event) => identity.id !== undefined && event.id === identity.id || identity.dedupeKey !== undefined && event.dedupeKey === identity.dedupeKey);
11303
+ return findEventByIdentity(events, identity);
11256
11304
  }
11257
11305
  async appendDelivery(result) {
11258
11306
  await this.init();
@@ -11303,6 +11351,83 @@ class JsonEventsStore {
11303
11351
  });
11304
11352
  }
11305
11353
  }
11354
+ function localJsonRuntime(dataDir = getEventsDataDir()) {
11355
+ return {
11356
+ mode: "local-files",
11357
+ name: "json-events-store",
11358
+ remote: false,
11359
+ localFiles: true,
11360
+ localSqlite: false,
11361
+ postgres: false,
11362
+ s3: false,
11363
+ aws: false,
11364
+ durable: true,
11365
+ idempotency: "best-effort-local",
11366
+ replayCursors: true,
11367
+ description: `Local JSON files in ${dataDir}; no SQLite, Postgres, S3, or AWS runtime is configured by this store.`
11368
+ };
11369
+ }
11370
+ function encodeLocalJsonEventCursor(offset, options = {}) {
11371
+ if (!Number.isInteger(offset) || offset < 0)
11372
+ throw new Error(`Invalid event cursor offset: ${offset}`);
11373
+ const payload = {
11374
+ offset,
11375
+ eventId: options.eventId,
11376
+ source: options.source,
11377
+ type: options.type
11378
+ };
11379
+ return `${LOCAL_JSON_EVENT_CURSOR_PREFIX}${Buffer2.from(JSON.stringify(payload), "utf-8").toString("base64url")}`;
11380
+ }
11381
+ function decodeLocalJsonEventCursor(cursor, options = {}) {
11382
+ if (!cursor)
11383
+ return 0;
11384
+ if (!cursor.startsWith(LOCAL_JSON_EVENT_CURSOR_PREFIX))
11385
+ throw new Error(`Invalid local JSON event cursor: ${cursor}`);
11386
+ const rawPayload = cursor.slice(LOCAL_JSON_EVENT_CURSOR_PREFIX.length);
11387
+ let payload;
11388
+ try {
11389
+ payload = JSON.parse(Buffer2.from(rawPayload, "base64url").toString("utf-8"));
11390
+ } catch {
11391
+ throw new Error(`Invalid local JSON event cursor: ${cursor}`);
11392
+ }
11393
+ const offset = payload.offset;
11394
+ if (!Number.isInteger(offset) || offset < 0)
11395
+ throw new Error(`Invalid local JSON event cursor: ${cursor}`);
11396
+ assertCursorFilter("eventId", payload.eventId, options.eventId);
11397
+ assertCursorFilter("source", payload.source, options.source);
11398
+ assertCursorFilter("type", payload.type, options.type);
11399
+ return offset;
11400
+ }
11401
+ function normalizeEventPageLimit(limit) {
11402
+ if (limit === undefined)
11403
+ return DEFAULT_EVENT_PAGE_LIMIT;
11404
+ if (!Number.isInteger(limit) || limit < 1)
11405
+ throw new Error(`Event page limit must be a positive integer, got ${limit}`);
11406
+ return Math.min(limit, MAX_EVENT_PAGE_LIMIT);
11407
+ }
11408
+ function queryEvents(events, options) {
11409
+ let rows = events;
11410
+ if (options.eventId)
11411
+ rows = rows.filter((event) => event.id === options.eventId);
11412
+ if (options.source)
11413
+ rows = rows.filter((event) => event.source === options.source);
11414
+ if (options.type)
11415
+ rows = rows.filter((event) => event.type === options.type);
11416
+ if (options.cursor) {
11417
+ const offset = decodeLocalJsonEventCursor(options.cursor, options);
11418
+ rows = rows.slice(offset);
11419
+ }
11420
+ if (options.limit !== undefined)
11421
+ rows = rows.slice(0, normalizeEventPageLimit(options.limit));
11422
+ return rows;
11423
+ }
11424
+ function assertCursorFilter(name, cursorValue, optionValue) {
11425
+ if (cursorValue !== optionValue)
11426
+ throw new Error(`Local JSON event cursor ${name} filter mismatch`);
11427
+ }
11428
+ function findEventByIdentity(events, identity) {
11429
+ return events.find((event) => identity.id !== undefined && event.id === identity.id || identity.dedupeKey !== undefined && event.dedupeKey === identity.dedupeKey);
11430
+ }
11306
11431
  function buildSignatureBase(timestamp2, body) {
11307
11432
  return `${timestamp2}.${body}`;
11308
11433
  }
@@ -11316,21 +11441,27 @@ function now2() {
11316
11441
  function truncate(value, max = 4096) {
11317
11442
  return value.length > max ? `${value.slice(0, max)}...` : value;
11318
11443
  }
11319
- function buildWebhookRequest(event, channel) {
11444
+ function buildWebhookRequest(event, channel, options = {}) {
11320
11445
  if (!channel.webhook)
11321
11446
  throw new Error(`Channel ${channel.id} has no webhook config`);
11447
+ for (const name of Object.keys(channel.webhook.headers ?? {})) {
11448
+ if (/^x-hasna-/i.test(name)) {
11449
+ throw new Error(`Webhook header ${name} is reserved for signed delivery metadata`);
11450
+ }
11451
+ }
11322
11452
  const body = JSON.stringify(event);
11323
- const timestamp2 = event.time;
11453
+ const timestamp2 = options.timestamp ?? new Date().toISOString();
11324
11454
  const headers = {
11325
11455
  "Content-Type": "application/json",
11326
11456
  "User-Agent": "@hasna/events",
11327
11457
  "X-Hasna-Event-Id": event.id,
11328
11458
  "X-Hasna-Event-Type": event.type,
11329
- "X-Hasna-Timestamp": timestamp2,
11330
- ...channel.webhook.headers
11459
+ ...channel.webhook.headers,
11460
+ "X-Hasna-Timestamp": timestamp2
11331
11461
  };
11332
- if (channel.webhook.secret) {
11333
- headers["X-Hasna-Signature"] = signPayload(channel.webhook.secret, timestamp2, body);
11462
+ const secret = options.secret ?? channel.webhook.secret;
11463
+ if (secret) {
11464
+ headers["X-Hasna-Signature"] = signPayload(secret, timestamp2, body);
11334
11465
  }
11335
11466
  return { body, headers };
11336
11467
  }
@@ -11338,7 +11469,21 @@ async function dispatchWebhook(event, channel, options = {}) {
11338
11469
  if (!channel.webhook)
11339
11470
  throw new Error(`Channel ${channel.id} has no webhook config`);
11340
11471
  const startedAt = now2();
11341
- const { body, headers } = buildWebhookRequest(event, channel);
11472
+ let secret = channel.webhook.secret;
11473
+ if (channel.webhook.secretRef) {
11474
+ if (!options.secretResolver) {
11475
+ return failedAttempt(startedAt, "Webhook secret reference has no runtime resolver");
11476
+ }
11477
+ try {
11478
+ secret = await options.secretResolver(channel.webhook.secretRef);
11479
+ } catch {
11480
+ return failedAttempt(startedAt, "Webhook secret reference could not be resolved");
11481
+ }
11482
+ if (!secret)
11483
+ return failedAttempt(startedAt, "Webhook secret reference could not be resolved");
11484
+ }
11485
+ const timestamp2 = (options.now?.() ?? new Date).toISOString();
11486
+ const { body, headers } = buildWebhookRequest(event, channel, { secret, timestamp: timestamp2 });
11342
11487
  const controller = new AbortController;
11343
11488
  const timeout = setTimeout(() => controller.abort(), channel.webhook.timeoutMs ?? 15000);
11344
11489
  try {
@@ -11370,6 +11515,15 @@ async function dispatchWebhook(event, channel, options = {}) {
11370
11515
  clearTimeout(timeout);
11371
11516
  }
11372
11517
  }
11518
+ function failedAttempt(startedAt, error) {
11519
+ return {
11520
+ attempt: 1,
11521
+ status: "failed",
11522
+ startedAt,
11523
+ completedAt: now2(),
11524
+ error
11525
+ };
11526
+ }
11373
11527
  async function dispatchCommand(event, channel) {
11374
11528
  if (!channel.command)
11375
11529
  throw new Error(`Channel ${channel.id} has no command config`);
@@ -11458,6 +11612,76 @@ function createDeliveryResult(event, channel, attempts) {
11458
11612
  completedAt: attempts.at(-1)?.completedAt ?? now2()
11459
11613
  };
11460
11614
  }
11615
+
11616
+ class EventTypeCatalog {
11617
+ definitions = new Map;
11618
+ register(definition) {
11619
+ this.definitions.set(definition.type, definition);
11620
+ return this;
11621
+ }
11622
+ unregister(type) {
11623
+ return this.definitions.delete(type);
11624
+ }
11625
+ has(type) {
11626
+ return this.definitions.has(type);
11627
+ }
11628
+ get(type) {
11629
+ return this.definitions.get(type);
11630
+ }
11631
+ list() {
11632
+ return [...this.definitions.values()];
11633
+ }
11634
+ validateEvent(event) {
11635
+ const definition = this.definitions.get(event.type);
11636
+ if (!definition)
11637
+ return { ok: true };
11638
+ return definition.validate(event.data, event);
11639
+ }
11640
+ assertEventValid(event) {
11641
+ const result = this.validateEvent(event);
11642
+ if (!result.ok) {
11643
+ throw new EventValidationError(event.type, result.issues);
11644
+ }
11645
+ }
11646
+ }
11647
+ function redactPaths(event, paths, replacement = "[REDACTED]") {
11648
+ if (paths.length === 0)
11649
+ return event;
11650
+ const copy = structuredClone(event);
11651
+ for (const path of paths) {
11652
+ setPath(copy, path, replacement);
11653
+ }
11654
+ return copy;
11655
+ }
11656
+ function redactSensitiveKeys(event, replacement = "[REDACTED]") {
11657
+ return redactValue2(event, replacement);
11658
+ }
11659
+ function shouldRedactKey(key) {
11660
+ return /secret|token|password|api[_-]?key|authorization/i.test(key);
11661
+ }
11662
+ function redactValue2(value, replacement) {
11663
+ if (Array.isArray(value))
11664
+ return value.map((item) => redactValue2(item, replacement));
11665
+ if (!value || typeof value !== "object")
11666
+ return value;
11667
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [
11668
+ key,
11669
+ shouldRedactKey(key) ? replacement : redactValue2(item, replacement)
11670
+ ]));
11671
+ }
11672
+ function setPath(input, path, replacement) {
11673
+ const parts = path.split(".");
11674
+ let cursor = input;
11675
+ for (const part of parts.slice(0, -1)) {
11676
+ const next = cursor[part];
11677
+ if (!next || typeof next !== "object")
11678
+ return;
11679
+ cursor = next;
11680
+ }
11681
+ const last = parts.at(-1);
11682
+ if (last && last in cursor)
11683
+ cursor[last] = replacement;
11684
+ }
11461
11685
  function createEvent(input) {
11462
11686
  return {
11463
11687
  id: input.id ?? randomUUID22(),
@@ -11478,10 +11702,18 @@ class EventsClient {
11478
11702
  store;
11479
11703
  redactors;
11480
11704
  transportOptions;
11705
+ catalog;
11706
+ validateCatalogTypes;
11481
11707
  constructor(options = {}) {
11482
11708
  this.store = options.store ?? new JsonEventsStore(options.dataDir);
11483
11709
  this.redactors = options.redactors ?? [];
11484
- this.transportOptions = { fetchImpl: options.fetchImpl };
11710
+ this.transportOptions = {
11711
+ fetchImpl: options.fetchImpl,
11712
+ secretResolver: options.secretResolver,
11713
+ now: options.now
11714
+ };
11715
+ this.catalog = options.catalog ?? defaultEventTypeCatalog;
11716
+ this.validateCatalogTypes = options.validateCatalogTypes ?? false;
11485
11717
  }
11486
11718
  async addChannel(input) {
11487
11719
  const timestamp2 = new Date().toISOString();
@@ -11499,18 +11731,40 @@ class EventsClient {
11499
11731
  }
11500
11732
  async emit(input, options = {}) {
11501
11733
  const event = options.redactSensitiveData === false ? createEvent(input) : redactSensitiveKeys(createEvent(input));
11502
- if (options.dedupe !== false) {
11503
- const existing = await this.store.findEventByIdentity({ id: input.id, dedupeKey: event.dedupeKey });
11504
- if (existing) {
11505
- return { event: existing, deliveries: [], deduped: true };
11506
- }
11507
- }
11508
- await this.store.appendEvent(event);
11509
- const deliveries = options.deliver === false ? [] : await this.deliver(event);
11510
- return { event, deliveries, deduped: false };
11511
- }
11512
- async listEvents() {
11513
- return this.store.listEvents();
11734
+ if (options.validate ?? this.validateCatalogTypes) {
11735
+ this.catalog.assertEventValid(event);
11736
+ }
11737
+ const append = await this.appendEvent(event, { dedupe: options.dedupe !== false });
11738
+ if (append.deduped) {
11739
+ return { event: append.event, deliveries: [], deduped: true };
11740
+ }
11741
+ const deliveries = options.deliver === false ? [] : await this.deliver(append.event);
11742
+ return { event: append.event, deliveries, deduped: false };
11743
+ }
11744
+ async listEvents(options = {}) {
11745
+ if (Object.keys(options).length === 0)
11746
+ return this.store.listEvents();
11747
+ return queryClientEvents(await this.store.listEvents(), options);
11748
+ }
11749
+ async listEventsPage(options = {}) {
11750
+ if (this.store.listEventsPage)
11751
+ return this.store.listEventsPage(options);
11752
+ const events = queryClientEvents(await this.store.listEvents(), {
11753
+ eventId: options.eventId,
11754
+ source: options.source,
11755
+ type: options.type
11756
+ });
11757
+ const offset = decodeLocalJsonEventCursor(options.cursor, options);
11758
+ const limit = normalizeEventPageLimit(options.limit);
11759
+ const pageEvents = events.slice(offset, offset + limit);
11760
+ const nextOffset = offset + pageEvents.length;
11761
+ const hasMore = nextOffset < events.length;
11762
+ return {
11763
+ events: pageEvents,
11764
+ cursor: options.cursor,
11765
+ nextCursor: hasMore ? encodeLocalJsonEventCursor(nextOffset, options) : undefined,
11766
+ hasMore
11767
+ };
11514
11768
  }
11515
11769
  async listDeliveries() {
11516
11770
  return this.store.listDeliveries();
@@ -11578,22 +11832,37 @@ class EventsClient {
11578
11832
  return result;
11579
11833
  }
11580
11834
  async replay(options = {}) {
11581
- const events = (await this.store.listEvents()).filter((event) => {
11582
- if (options.eventId && event.id !== options.eventId)
11583
- return false;
11584
- if (options.source && event.source !== options.source)
11585
- return false;
11586
- if (options.type && event.type !== options.type)
11587
- return false;
11588
- return true;
11589
- });
11835
+ const page = options.cursor || options.limit !== undefined ? await this.listEventsPage(options) : { events: await this.listEvents(options), hasMore: false };
11590
11836
  if (options.dryRun)
11591
- return { events, deliveries: [] };
11837
+ return { events: page.events, deliveries: [], cursor: page.cursor, nextCursor: page.nextCursor, hasMore: page.hasMore };
11592
11838
  const deliveries = [];
11593
- for (const event of events) {
11839
+ for (const event of page.events) {
11594
11840
  deliveries.push(...await this.deliver(event));
11595
11841
  }
11596
- return { events, deliveries };
11842
+ return { events: page.events, deliveries, cursor: page.cursor, nextCursor: page.nextCursor, hasMore: page.hasMore };
11843
+ }
11844
+ async appendEvent(event, options) {
11845
+ if (this.store.appendEventOnce) {
11846
+ return this.store.appendEventOnce(event, { dedupe: options.dedupe });
11847
+ }
11848
+ if (options.dedupe) {
11849
+ const existing = await this.store.findEventByIdentity({ id: event.id, dedupeKey: event.dedupeKey });
11850
+ if (existing) {
11851
+ return {
11852
+ event: existing,
11853
+ stored: false,
11854
+ deduped: true,
11855
+ identity: { id: existing.id, dedupeKey: existing.dedupeKey }
11856
+ };
11857
+ }
11858
+ }
11859
+ const stored = await this.store.appendEvent(event);
11860
+ return {
11861
+ event: stored,
11862
+ stored: true,
11863
+ deduped: false,
11864
+ identity: { id: stored.id, dedupeKey: stored.dedupeKey }
11865
+ };
11597
11866
  }
11598
11867
  async applyRedaction(event, channel) {
11599
11868
  let next = redactPaths(event, channel.redact?.paths ?? [], channel.redact?.replacement ?? "[REDACTED]");
@@ -11620,43 +11889,19 @@ class EventsClient {
11620
11889
  return createDeliveryResult(event, channel, attempts);
11621
11890
  }
11622
11891
  }
11623
- function redactPaths(event, paths, replacement = "[REDACTED]") {
11624
- if (paths.length === 0)
11625
- return event;
11626
- const copy = structuredClone(event);
11627
- for (const path of paths) {
11628
- setPath(copy, path, replacement);
11629
- }
11630
- return copy;
11631
- }
11632
- function redactSensitiveKeys(event, replacement = "[REDACTED]") {
11633
- return redactValue2(event, replacement);
11634
- }
11635
- function shouldRedactKey(key) {
11636
- return /secret|token|password|api[_-]?key|authorization/i.test(key);
11637
- }
11638
- function redactValue2(value, replacement) {
11639
- if (Array.isArray(value))
11640
- return value.map((item) => redactValue2(item, replacement));
11641
- if (!value || typeof value !== "object")
11642
- return value;
11643
- return Object.fromEntries(Object.entries(value).map(([key, item]) => [
11644
- key,
11645
- shouldRedactKey(key) ? replacement : redactValue2(item, replacement)
11646
- ]));
11647
- }
11648
- function setPath(input, path, replacement) {
11649
- const parts = path.split(".");
11650
- let cursor = input;
11651
- for (const part of parts.slice(0, -1)) {
11652
- const next = cursor[part];
11653
- if (!next || typeof next !== "object")
11654
- return;
11655
- cursor = next;
11656
- }
11657
- const last = parts.at(-1);
11658
- if (last && last in cursor)
11659
- cursor[last] = replacement;
11892
+ function queryClientEvents(events, options) {
11893
+ let rows = events;
11894
+ if (options.eventId)
11895
+ rows = rows.filter((event) => event.id === options.eventId);
11896
+ if (options.source)
11897
+ rows = rows.filter((event) => event.source === options.source);
11898
+ if (options.type)
11899
+ rows = rows.filter((event) => event.type === options.type);
11900
+ if (options.cursor)
11901
+ rows = rows.slice(decodeLocalJsonEventCursor(options.cursor, options));
11902
+ if (options.limit !== undefined)
11903
+ rows = rows.slice(0, normalizeEventPageLimit(options.limit));
11904
+ return rows;
11660
11905
  }
11661
11906
  function normalizeTime(value) {
11662
11907
  if (!value)
@@ -11670,9 +11915,22 @@ function normalizeRetryPolicy(policy) {
11670
11915
  multiplier: Math.max(1, policy?.multiplier ?? 2)
11671
11916
  };
11672
11917
  }
11673
- var HASNA_EVENTS_DIR_ENV = "HASNA_EVENTS_DIR", HASNA_EVENTS_HOME_ENV = "HASNA_EVENTS_HOME", DEFAULT_SIGNATURE_TOLERANCE_MS;
11918
+ var HASNA_EVENTS_DIR_ENV = "HASNA_EVENTS_DIR", HASNA_EVENTS_HOME_ENV = "HASNA_EVENTS_HOME", LOCAL_JSON_EVENT_CURSOR_PREFIX = "local-json-v1:", DEFAULT_EVENT_PAGE_LIMIT = 100, MAX_EVENT_PAGE_LIMIT = 1000, DEFAULT_SIGNATURE_TOLERANCE_MS, EventValidationError, defaultEventTypeCatalog, APP_EVENT_V1_MAX_DATA_BYTES;
11674
11919
  var init_dist = __esm(() => {
11675
11920
  DEFAULT_SIGNATURE_TOLERANCE_MS = 5 * 60 * 1000;
11921
+ EventValidationError = class EventValidationError extends Error {
11922
+ eventType;
11923
+ issues;
11924
+ constructor(eventType, issues) {
11925
+ const detail = issues.map((issue) => `${issue.path || "<root>"}: ${issue.message}`).join("; ");
11926
+ super(`Event validation failed for type "${eventType}": ${detail}`);
11927
+ this.name = "EventValidationError";
11928
+ this.eventType = eventType;
11929
+ this.issues = issues;
11930
+ }
11931
+ };
11932
+ defaultEventTypeCatalog = new EventTypeCatalog;
11933
+ APP_EVENT_V1_MAX_DATA_BYTES = 32 * 1024;
11676
11934
  });
11677
11935
 
11678
11936
  // src/db/task-lists.ts
@@ -12511,8 +12769,12 @@ async function deliverWebhook(wh, event, body, attempt, db) {
12511
12769
  const sig = await crypto.subtle.sign("HMAC", key, encoder.encode(body));
12512
12770
  headers["X-Webhook-Signature"] = Array.from(new Uint8Array(sig)).map((b) => b.toString(16).padStart(2, "0")).join("");
12513
12771
  }
12514
- const resp = await fetch(wh.url, { method: "POST", headers, body });
12772
+ const resp = await fetch(wh.url, { method: "POST", headers, body, redirect: "manual" });
12515
12773
  const respText = await resp.text().catch(() => "");
12774
+ if (resp.status >= 300 && resp.status < 400) {
12775
+ logDelivery(db, wh.id, event, body, resp.status, "Blocked: webhook URL returned a 3xx redirect; redirects are never followed (SSRF prevention)", attempt);
12776
+ return;
12777
+ }
12516
12778
  logDelivery(db, wh.id, event, body, resp.status, respText.slice(0, 1000), attempt);
12517
12779
  if (resp.status >= 400 && attempt < MAX_RETRY_ATTEMPTS) {
12518
12780
  const delay = RETRY_BASE_DELAY_MS * Math.pow(2, attempt - 1);
@@ -36102,7 +36364,7 @@ var package_default;
36102
36364
  var init_package = __esm(() => {
36103
36365
  package_default = {
36104
36366
  name: "@hasna/todos",
36105
- version: "0.15.46",
36367
+ version: "0.15.47",
36106
36368
  description: "Universal task management for AI coding agents - CLI + MCP server + interactive TUI",
36107
36369
  type: "module",
36108
36370
  main: "dist/index.js",
@@ -36227,11 +36489,12 @@ var init_package = __esm(() => {
36227
36489
  commander: "^13.1.0",
36228
36490
  ink: "^5.2.0",
36229
36491
  react: "^18.3.1",
36230
- zod: "^3.24.2"
36492
+ zod: "3.25.76"
36231
36493
  },
36232
36494
  overrides: {
36233
36495
  ajv: "8.20.0",
36234
- "fast-uri": "3.1.2"
36496
+ "fast-uri": "3.1.2",
36497
+ zod: "3.25.76"
36235
36498
  },
36236
36499
  devDependencies: {
36237
36500
  "@types/bun": "^1.2.4",
@@ -47490,9 +47753,11 @@ class PostgresJsonRecordStore {
47490
47753
  async ensureSchema() {
47491
47754
  if (!this.schemaReady) {
47492
47755
  this.schemaReady = (async () => {
47493
- for (const sql of postgresTodosSyncSchemaSql(this.tableName, this.cursorTableName)) {
47494
- await this.options.client.query(sql);
47495
- }
47756
+ await retryOnTransientPostgresError(async () => {
47757
+ for (const sql of postgresTodosSyncSchemaSql(this.tableName, this.cursorTableName)) {
47758
+ await this.options.client.query(sql);
47759
+ }
47760
+ });
47496
47761
  })().catch((error) => {
47497
47762
  this.schemaReady = null;
47498
47763
  throw error;
@@ -47906,13 +48171,14 @@ class PostgresJsonRecordStore {
47906
48171
  throw new Error(divergentAuditHistoryReplayError(value.id));
47907
48172
  }
47908
48173
  async withTaskParentIntegrityTransaction(fn) {
47909
- if (typeof this.options.client.transaction !== "function") {
48174
+ const transaction = this.options.client.transaction;
48175
+ if (typeof transaction !== "function") {
47910
48176
  throw new Error("TASK_PARENT_ATOMICITY_UNAVAILABLE: PostgreSQL parent writes and task deletion require transaction(callback)");
47911
48177
  }
47912
- return this.options.client.transaction(async (client) => {
48178
+ return retryOnTransientPostgresError(() => transaction(async (client) => {
47913
48179
  await client.query("/* todos:task-parent-integrity-lock */ SELECT pg_advisory_xact_lock(hashtextextended($1 || ':task-parent-integrity', 0))", [this.service]);
47914
48180
  return fn(client);
47915
- });
48181
+ }));
47916
48182
  }
47917
48183
  async upsertTaskWithPlanMembershipGuard(value, guardedPlanIds, explicitProject, context = {}, parentGuard, queryClient) {
47918
48184
  const planIds = [...new Set(guardedPlanIds.filter(Boolean))].sort();
@@ -49795,6 +50061,35 @@ function compareClock(left, right) {
49795
50061
  function numberValue3(value) {
49796
50062
  return typeof value === "number" && Number.isSafeInteger(value) ? value : null;
49797
50063
  }
50064
+ function isTransientPostgresError(error) {
50065
+ if (typeof error !== "object" || error === null)
50066
+ return false;
50067
+ const candidate = error;
50068
+ const states = [candidate.code, candidate.errno, candidate.sqlState, candidate.sqlstate];
50069
+ if (typeof candidate.cause === "object" && candidate.cause !== null) {
50070
+ const cause = candidate.cause;
50071
+ states.push(cause.code, cause.errno, cause.sqlState, cause.sqlstate);
50072
+ }
50073
+ if (states.some((state) => typeof state === "string" && TRANSIENT_POSTGRES_SQLSTATES.has(state))) {
50074
+ return true;
50075
+ }
50076
+ const message = typeof candidate.message === "string" ? candidate.message : candidate.cause?.message;
50077
+ return typeof message === "string" && TRANSIENT_POSTGRES_MESSAGE_MARKERS.some((marker) => message.includes(marker));
50078
+ }
50079
+ async function retryOnTransientPostgresError(fn, attempts = 2, delayMs = 150) {
50080
+ let lastError;
50081
+ for (let attempt = 1;attempt <= attempts; attempt += 1) {
50082
+ try {
50083
+ return await fn();
50084
+ } catch (error) {
50085
+ lastError = error;
50086
+ if (!isTransientPostgresError(error) || attempt === attempts)
50087
+ throw error;
50088
+ await new Promise((resolve16) => setTimeout(resolve16, delayMs * attempt));
50089
+ }
50090
+ }
50091
+ throw lastError;
50092
+ }
49798
50093
  function isPostgresUniqueViolation(error) {
49799
50094
  if (typeof error !== "object" || error === null)
49800
50095
  return false;
@@ -49814,7 +50109,7 @@ function postgresConstraintName(error) {
49814
50109
  const constraint = candidate.constraint ?? candidate.constraint_name ?? cause?.constraint ?? cause?.constraint_name;
49815
50110
  return typeof constraint === "string" ? constraint : "";
49816
50111
  }
49817
- var CLOUD_LOCK_EXPIRY_MINUTES = 30, TASK_ORDER_TIEBREAK = "CASE payload->>'priority' WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 ELSE 4 END ASC, payload->>'created_at' ASC, payload->>'id' ASC", TASK_ORDER_BY;
50112
+ var CLOUD_LOCK_EXPIRY_MINUTES = 30, TASK_ORDER_TIEBREAK = "CASE payload->>'priority' WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 ELSE 4 END ASC, payload->>'created_at' ASC, payload->>'id' ASC", TASK_ORDER_BY, TRANSIENT_POSTGRES_SQLSTATES, TRANSIENT_POSTGRES_MESSAGE_MARKERS;
49818
50113
  var init_postgres_adapter = __esm(() => {
49819
50114
  init_types();
49820
50115
  init_creator_identity();
@@ -49828,6 +50123,12 @@ var init_postgres_adapter = __esm(() => {
49828
50123
  init_audit_history_import();
49829
50124
  init_canonical();
49830
50125
  TASK_ORDER_BY = `ORDER BY ${TASK_ORDER_TIEBREAK}`;
50126
+ TRANSIENT_POSTGRES_SQLSTATES = new Set(["55P03", "40P01", "40001"]);
50127
+ TRANSIENT_POSTGRES_MESSAGE_MARKERS = [
50128
+ "canceling statement due to lock timeout",
50129
+ "deadlock detected",
50130
+ "could not serialize access"
50131
+ ];
49831
50132
  });
49832
50133
 
49833
50134
  // src/pr-groups/postgres.ts
package/dist/mcp.js CHANGED
@@ -41,7 +41,7 @@ var __require = import.meta.require;
41
41
  // package.json
42
42
  var package_default = {
43
43
  name: "@hasna/todos",
44
- version: "0.15.46",
44
+ version: "0.15.47",
45
45
  description: "Universal task management for AI coding agents - CLI + MCP server + interactive TUI",
46
46
  type: "module",
47
47
  main: "dist/index.js",
@@ -166,11 +166,12 @@ var package_default = {
166
166
  commander: "^13.1.0",
167
167
  ink: "^5.2.0",
168
168
  react: "^18.3.1",
169
- zod: "^3.24.2"
169
+ zod: "3.25.76"
170
170
  },
171
171
  overrides: {
172
172
  ajv: "8.20.0",
173
- "fast-uri": "3.1.2"
173
+ "fast-uri": "3.1.2",
174
+ zod: "3.25.76"
174
175
  },
175
176
  devDependencies: {
176
177
  "@types/bun": "^1.2.4",