@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.
@@ -6607,8 +6607,9 @@ var init_event_hooks = __esm(() => {
6607
6607
  VALID_TARGETS = new Set(["stdout", "file", "socket", "script"]);
6608
6608
  });
6609
6609
 
6610
- // node_modules/.bun/@hasna+events@0.1.11/node_modules/@hasna/events/dist/index.js
6610
+ // node_modules/.bun/@hasna+events@0.1.16/node_modules/@hasna/events/dist/index.js
6611
6611
  import { chmod, mkdir, readFile, rename, writeFile } from "fs/promises";
6612
+ import { Buffer as Buffer2 } from "buffer";
6612
6613
  import { existsSync as existsSync6 } from "fs";
6613
6614
  import { homedir as homedir2 } from "os";
6614
6615
  import { join as join4 } from "path";
@@ -6717,11 +6718,13 @@ function getEventsDataDir(override) {
6717
6718
 
6718
6719
  class JsonEventsStore {
6719
6720
  dataDir;
6721
+ runtime;
6720
6722
  channelsPath;
6721
6723
  eventsPath;
6722
6724
  deliveriesPath;
6723
6725
  constructor(dataDir = getEventsDataDir()) {
6724
6726
  this.dataDir = dataDir;
6727
+ this.runtime = localJsonRuntime(dataDir);
6725
6728
  this.channelsPath = join4(dataDir, "channels.json");
6726
6729
  this.eventsPath = join4(dataDir, "events.json");
6727
6730
  this.deliveriesPath = join4(dataDir, "deliveries.json");
@@ -6769,13 +6772,58 @@ class JsonEventsStore {
6769
6772
  await this.writeJson(this.eventsPath, events);
6770
6773
  return event;
6771
6774
  }
6772
- async listEvents() {
6775
+ async appendEventOnce(event, options = {}) {
6773
6776
  await this.init();
6774
- return this.readJson(this.eventsPath, []);
6777
+ const events = await this.readJson(this.eventsPath, []);
6778
+ const dedupe = options.dedupe !== false;
6779
+ if (dedupe) {
6780
+ const existing = findEventByIdentity(events, { id: event.id, dedupeKey: event.dedupeKey });
6781
+ if (existing) {
6782
+ return {
6783
+ event: existing,
6784
+ stored: false,
6785
+ deduped: true,
6786
+ identity: { id: existing.id, dedupeKey: existing.dedupeKey }
6787
+ };
6788
+ }
6789
+ }
6790
+ events.push(event);
6791
+ await this.writeJson(this.eventsPath, events);
6792
+ return {
6793
+ event,
6794
+ stored: true,
6795
+ deduped: false,
6796
+ identity: { id: event.id, dedupeKey: event.dedupeKey }
6797
+ };
6798
+ }
6799
+ async listEvents(options = {}) {
6800
+ await this.init();
6801
+ const events = await this.readJson(this.eventsPath, []);
6802
+ return queryEvents(events, options);
6803
+ }
6804
+ async listEventsPage(options = {}) {
6805
+ await this.init();
6806
+ const events = await this.readJson(this.eventsPath, []);
6807
+ const queried = queryEvents(events, {
6808
+ eventId: options.eventId,
6809
+ source: options.source,
6810
+ type: options.type
6811
+ });
6812
+ const offset = decodeLocalJsonEventCursor(options.cursor, options);
6813
+ const limit = normalizeEventPageLimit(options.limit);
6814
+ const pageEvents = queried.slice(offset, offset + limit);
6815
+ const nextOffset = offset + pageEvents.length;
6816
+ const hasMore = nextOffset < queried.length;
6817
+ return {
6818
+ events: pageEvents,
6819
+ cursor: options.cursor,
6820
+ nextCursor: hasMore ? encodeLocalJsonEventCursor(nextOffset, options) : undefined,
6821
+ hasMore
6822
+ };
6775
6823
  }
6776
6824
  async findEventByIdentity(identity) {
6777
6825
  const events = await this.listEvents();
6778
- return events.find((event) => identity.id !== undefined && event.id === identity.id || identity.dedupeKey !== undefined && event.dedupeKey === identity.dedupeKey);
6826
+ return findEventByIdentity(events, identity);
6779
6827
  }
6780
6828
  async appendDelivery(result) {
6781
6829
  await this.init();
@@ -6826,6 +6874,83 @@ class JsonEventsStore {
6826
6874
  });
6827
6875
  }
6828
6876
  }
6877
+ function localJsonRuntime(dataDir = getEventsDataDir()) {
6878
+ return {
6879
+ mode: "local-files",
6880
+ name: "json-events-store",
6881
+ remote: false,
6882
+ localFiles: true,
6883
+ localSqlite: false,
6884
+ postgres: false,
6885
+ s3: false,
6886
+ aws: false,
6887
+ durable: true,
6888
+ idempotency: "best-effort-local",
6889
+ replayCursors: true,
6890
+ description: `Local JSON files in ${dataDir}; no SQLite, Postgres, S3, or AWS runtime is configured by this store.`
6891
+ };
6892
+ }
6893
+ function encodeLocalJsonEventCursor(offset, options = {}) {
6894
+ if (!Number.isInteger(offset) || offset < 0)
6895
+ throw new Error(`Invalid event cursor offset: ${offset}`);
6896
+ const payload = {
6897
+ offset,
6898
+ eventId: options.eventId,
6899
+ source: options.source,
6900
+ type: options.type
6901
+ };
6902
+ return `${LOCAL_JSON_EVENT_CURSOR_PREFIX}${Buffer2.from(JSON.stringify(payload), "utf-8").toString("base64url")}`;
6903
+ }
6904
+ function decodeLocalJsonEventCursor(cursor, options = {}) {
6905
+ if (!cursor)
6906
+ return 0;
6907
+ if (!cursor.startsWith(LOCAL_JSON_EVENT_CURSOR_PREFIX))
6908
+ throw new Error(`Invalid local JSON event cursor: ${cursor}`);
6909
+ const rawPayload = cursor.slice(LOCAL_JSON_EVENT_CURSOR_PREFIX.length);
6910
+ let payload;
6911
+ try {
6912
+ payload = JSON.parse(Buffer2.from(rawPayload, "base64url").toString("utf-8"));
6913
+ } catch {
6914
+ throw new Error(`Invalid local JSON event cursor: ${cursor}`);
6915
+ }
6916
+ const offset = payload.offset;
6917
+ if (!Number.isInteger(offset) || offset < 0)
6918
+ throw new Error(`Invalid local JSON event cursor: ${cursor}`);
6919
+ assertCursorFilter("eventId", payload.eventId, options.eventId);
6920
+ assertCursorFilter("source", payload.source, options.source);
6921
+ assertCursorFilter("type", payload.type, options.type);
6922
+ return offset;
6923
+ }
6924
+ function normalizeEventPageLimit(limit) {
6925
+ if (limit === undefined)
6926
+ return DEFAULT_EVENT_PAGE_LIMIT;
6927
+ if (!Number.isInteger(limit) || limit < 1)
6928
+ throw new Error(`Event page limit must be a positive integer, got ${limit}`);
6929
+ return Math.min(limit, MAX_EVENT_PAGE_LIMIT);
6930
+ }
6931
+ function queryEvents(events, options) {
6932
+ let rows = events;
6933
+ if (options.eventId)
6934
+ rows = rows.filter((event) => event.id === options.eventId);
6935
+ if (options.source)
6936
+ rows = rows.filter((event) => event.source === options.source);
6937
+ if (options.type)
6938
+ rows = rows.filter((event) => event.type === options.type);
6939
+ if (options.cursor) {
6940
+ const offset = decodeLocalJsonEventCursor(options.cursor, options);
6941
+ rows = rows.slice(offset);
6942
+ }
6943
+ if (options.limit !== undefined)
6944
+ rows = rows.slice(0, normalizeEventPageLimit(options.limit));
6945
+ return rows;
6946
+ }
6947
+ function assertCursorFilter(name, cursorValue, optionValue) {
6948
+ if (cursorValue !== optionValue)
6949
+ throw new Error(`Local JSON event cursor ${name} filter mismatch`);
6950
+ }
6951
+ function findEventByIdentity(events, identity) {
6952
+ return events.find((event) => identity.id !== undefined && event.id === identity.id || identity.dedupeKey !== undefined && event.dedupeKey === identity.dedupeKey);
6953
+ }
6829
6954
  function buildSignatureBase(timestamp2, body) {
6830
6955
  return `${timestamp2}.${body}`;
6831
6956
  }
@@ -6839,21 +6964,27 @@ function now2() {
6839
6964
  function truncate(value, max = 4096) {
6840
6965
  return value.length > max ? `${value.slice(0, max)}...` : value;
6841
6966
  }
6842
- function buildWebhookRequest(event, channel) {
6967
+ function buildWebhookRequest(event, channel, options = {}) {
6843
6968
  if (!channel.webhook)
6844
6969
  throw new Error(`Channel ${channel.id} has no webhook config`);
6970
+ for (const name of Object.keys(channel.webhook.headers ?? {})) {
6971
+ if (/^x-hasna-/i.test(name)) {
6972
+ throw new Error(`Webhook header ${name} is reserved for signed delivery metadata`);
6973
+ }
6974
+ }
6845
6975
  const body = JSON.stringify(event);
6846
- const timestamp2 = event.time;
6976
+ const timestamp2 = options.timestamp ?? new Date().toISOString();
6847
6977
  const headers = {
6848
6978
  "Content-Type": "application/json",
6849
6979
  "User-Agent": "@hasna/events",
6850
6980
  "X-Hasna-Event-Id": event.id,
6851
6981
  "X-Hasna-Event-Type": event.type,
6852
- "X-Hasna-Timestamp": timestamp2,
6853
- ...channel.webhook.headers
6982
+ ...channel.webhook.headers,
6983
+ "X-Hasna-Timestamp": timestamp2
6854
6984
  };
6855
- if (channel.webhook.secret) {
6856
- headers["X-Hasna-Signature"] = signPayload(channel.webhook.secret, timestamp2, body);
6985
+ const secret = options.secret ?? channel.webhook.secret;
6986
+ if (secret) {
6987
+ headers["X-Hasna-Signature"] = signPayload(secret, timestamp2, body);
6857
6988
  }
6858
6989
  return { body, headers };
6859
6990
  }
@@ -6861,7 +6992,21 @@ async function dispatchWebhook(event, channel, options = {}) {
6861
6992
  if (!channel.webhook)
6862
6993
  throw new Error(`Channel ${channel.id} has no webhook config`);
6863
6994
  const startedAt = now2();
6864
- const { body, headers } = buildWebhookRequest(event, channel);
6995
+ let secret = channel.webhook.secret;
6996
+ if (channel.webhook.secretRef) {
6997
+ if (!options.secretResolver) {
6998
+ return failedAttempt(startedAt, "Webhook secret reference has no runtime resolver");
6999
+ }
7000
+ try {
7001
+ secret = await options.secretResolver(channel.webhook.secretRef);
7002
+ } catch {
7003
+ return failedAttempt(startedAt, "Webhook secret reference could not be resolved");
7004
+ }
7005
+ if (!secret)
7006
+ return failedAttempt(startedAt, "Webhook secret reference could not be resolved");
7007
+ }
7008
+ const timestamp2 = (options.now?.() ?? new Date).toISOString();
7009
+ const { body, headers } = buildWebhookRequest(event, channel, { secret, timestamp: timestamp2 });
6865
7010
  const controller = new AbortController;
6866
7011
  const timeout = setTimeout(() => controller.abort(), channel.webhook.timeoutMs ?? 15000);
6867
7012
  try {
@@ -6893,6 +7038,15 @@ async function dispatchWebhook(event, channel, options = {}) {
6893
7038
  clearTimeout(timeout);
6894
7039
  }
6895
7040
  }
7041
+ function failedAttempt(startedAt, error) {
7042
+ return {
7043
+ attempt: 1,
7044
+ status: "failed",
7045
+ startedAt,
7046
+ completedAt: now2(),
7047
+ error
7048
+ };
7049
+ }
6896
7050
  async function dispatchCommand(event, channel) {
6897
7051
  if (!channel.command)
6898
7052
  throw new Error(`Channel ${channel.id} has no command config`);
@@ -6981,6 +7135,76 @@ function createDeliveryResult(event, channel, attempts) {
6981
7135
  completedAt: attempts.at(-1)?.completedAt ?? now2()
6982
7136
  };
6983
7137
  }
7138
+
7139
+ class EventTypeCatalog {
7140
+ definitions = new Map;
7141
+ register(definition) {
7142
+ this.definitions.set(definition.type, definition);
7143
+ return this;
7144
+ }
7145
+ unregister(type) {
7146
+ return this.definitions.delete(type);
7147
+ }
7148
+ has(type) {
7149
+ return this.definitions.has(type);
7150
+ }
7151
+ get(type) {
7152
+ return this.definitions.get(type);
7153
+ }
7154
+ list() {
7155
+ return [...this.definitions.values()];
7156
+ }
7157
+ validateEvent(event) {
7158
+ const definition = this.definitions.get(event.type);
7159
+ if (!definition)
7160
+ return { ok: true };
7161
+ return definition.validate(event.data, event);
7162
+ }
7163
+ assertEventValid(event) {
7164
+ const result = this.validateEvent(event);
7165
+ if (!result.ok) {
7166
+ throw new EventValidationError(event.type, result.issues);
7167
+ }
7168
+ }
7169
+ }
7170
+ function redactPaths(event, paths, replacement = "[REDACTED]") {
7171
+ if (paths.length === 0)
7172
+ return event;
7173
+ const copy = structuredClone(event);
7174
+ for (const path of paths) {
7175
+ setPath(copy, path, replacement);
7176
+ }
7177
+ return copy;
7178
+ }
7179
+ function redactSensitiveKeys(event, replacement = "[REDACTED]") {
7180
+ return redactValue2(event, replacement);
7181
+ }
7182
+ function shouldRedactKey(key) {
7183
+ return /secret|token|password|api[_-]?key|authorization/i.test(key);
7184
+ }
7185
+ function redactValue2(value, replacement) {
7186
+ if (Array.isArray(value))
7187
+ return value.map((item) => redactValue2(item, replacement));
7188
+ if (!value || typeof value !== "object")
7189
+ return value;
7190
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [
7191
+ key,
7192
+ shouldRedactKey(key) ? replacement : redactValue2(item, replacement)
7193
+ ]));
7194
+ }
7195
+ function setPath(input, path, replacement) {
7196
+ const parts = path.split(".");
7197
+ let cursor = input;
7198
+ for (const part of parts.slice(0, -1)) {
7199
+ const next = cursor[part];
7200
+ if (!next || typeof next !== "object")
7201
+ return;
7202
+ cursor = next;
7203
+ }
7204
+ const last = parts.at(-1);
7205
+ if (last && last in cursor)
7206
+ cursor[last] = replacement;
7207
+ }
6984
7208
  function createEvent(input) {
6985
7209
  return {
6986
7210
  id: input.id ?? randomUUID22(),
@@ -7001,10 +7225,18 @@ class EventsClient {
7001
7225
  store;
7002
7226
  redactors;
7003
7227
  transportOptions;
7228
+ catalog;
7229
+ validateCatalogTypes;
7004
7230
  constructor(options = {}) {
7005
7231
  this.store = options.store ?? new JsonEventsStore(options.dataDir);
7006
7232
  this.redactors = options.redactors ?? [];
7007
- this.transportOptions = { fetchImpl: options.fetchImpl };
7233
+ this.transportOptions = {
7234
+ fetchImpl: options.fetchImpl,
7235
+ secretResolver: options.secretResolver,
7236
+ now: options.now
7237
+ };
7238
+ this.catalog = options.catalog ?? defaultEventTypeCatalog;
7239
+ this.validateCatalogTypes = options.validateCatalogTypes ?? false;
7008
7240
  }
7009
7241
  async addChannel(input) {
7010
7242
  const timestamp2 = new Date().toISOString();
@@ -7022,18 +7254,40 @@ class EventsClient {
7022
7254
  }
7023
7255
  async emit(input, options = {}) {
7024
7256
  const event = options.redactSensitiveData === false ? createEvent(input) : redactSensitiveKeys(createEvent(input));
7025
- if (options.dedupe !== false) {
7026
- const existing = await this.store.findEventByIdentity({ id: input.id, dedupeKey: event.dedupeKey });
7027
- if (existing) {
7028
- return { event: existing, deliveries: [], deduped: true };
7029
- }
7030
- }
7031
- await this.store.appendEvent(event);
7032
- const deliveries = options.deliver === false ? [] : await this.deliver(event);
7033
- return { event, deliveries, deduped: false };
7034
- }
7035
- async listEvents() {
7036
- return this.store.listEvents();
7257
+ if (options.validate ?? this.validateCatalogTypes) {
7258
+ this.catalog.assertEventValid(event);
7259
+ }
7260
+ const append = await this.appendEvent(event, { dedupe: options.dedupe !== false });
7261
+ if (append.deduped) {
7262
+ return { event: append.event, deliveries: [], deduped: true };
7263
+ }
7264
+ const deliveries = options.deliver === false ? [] : await this.deliver(append.event);
7265
+ return { event: append.event, deliveries, deduped: false };
7266
+ }
7267
+ async listEvents(options = {}) {
7268
+ if (Object.keys(options).length === 0)
7269
+ return this.store.listEvents();
7270
+ return queryClientEvents(await this.store.listEvents(), options);
7271
+ }
7272
+ async listEventsPage(options = {}) {
7273
+ if (this.store.listEventsPage)
7274
+ return this.store.listEventsPage(options);
7275
+ const events = queryClientEvents(await this.store.listEvents(), {
7276
+ eventId: options.eventId,
7277
+ source: options.source,
7278
+ type: options.type
7279
+ });
7280
+ const offset = decodeLocalJsonEventCursor(options.cursor, options);
7281
+ const limit = normalizeEventPageLimit(options.limit);
7282
+ const pageEvents = events.slice(offset, offset + limit);
7283
+ const nextOffset = offset + pageEvents.length;
7284
+ const hasMore = nextOffset < events.length;
7285
+ return {
7286
+ events: pageEvents,
7287
+ cursor: options.cursor,
7288
+ nextCursor: hasMore ? encodeLocalJsonEventCursor(nextOffset, options) : undefined,
7289
+ hasMore
7290
+ };
7037
7291
  }
7038
7292
  async listDeliveries() {
7039
7293
  return this.store.listDeliveries();
@@ -7101,22 +7355,37 @@ class EventsClient {
7101
7355
  return result;
7102
7356
  }
7103
7357
  async replay(options = {}) {
7104
- const events = (await this.store.listEvents()).filter((event) => {
7105
- if (options.eventId && event.id !== options.eventId)
7106
- return false;
7107
- if (options.source && event.source !== options.source)
7108
- return false;
7109
- if (options.type && event.type !== options.type)
7110
- return false;
7111
- return true;
7112
- });
7358
+ const page = options.cursor || options.limit !== undefined ? await this.listEventsPage(options) : { events: await this.listEvents(options), hasMore: false };
7113
7359
  if (options.dryRun)
7114
- return { events, deliveries: [] };
7360
+ return { events: page.events, deliveries: [], cursor: page.cursor, nextCursor: page.nextCursor, hasMore: page.hasMore };
7115
7361
  const deliveries = [];
7116
- for (const event of events) {
7362
+ for (const event of page.events) {
7117
7363
  deliveries.push(...await this.deliver(event));
7118
7364
  }
7119
- return { events, deliveries };
7365
+ return { events: page.events, deliveries, cursor: page.cursor, nextCursor: page.nextCursor, hasMore: page.hasMore };
7366
+ }
7367
+ async appendEvent(event, options) {
7368
+ if (this.store.appendEventOnce) {
7369
+ return this.store.appendEventOnce(event, { dedupe: options.dedupe });
7370
+ }
7371
+ if (options.dedupe) {
7372
+ const existing = await this.store.findEventByIdentity({ id: event.id, dedupeKey: event.dedupeKey });
7373
+ if (existing) {
7374
+ return {
7375
+ event: existing,
7376
+ stored: false,
7377
+ deduped: true,
7378
+ identity: { id: existing.id, dedupeKey: existing.dedupeKey }
7379
+ };
7380
+ }
7381
+ }
7382
+ const stored = await this.store.appendEvent(event);
7383
+ return {
7384
+ event: stored,
7385
+ stored: true,
7386
+ deduped: false,
7387
+ identity: { id: stored.id, dedupeKey: stored.dedupeKey }
7388
+ };
7120
7389
  }
7121
7390
  async applyRedaction(event, channel) {
7122
7391
  let next = redactPaths(event, channel.redact?.paths ?? [], channel.redact?.replacement ?? "[REDACTED]");
@@ -7143,43 +7412,19 @@ class EventsClient {
7143
7412
  return createDeliveryResult(event, channel, attempts);
7144
7413
  }
7145
7414
  }
7146
- function redactPaths(event, paths, replacement = "[REDACTED]") {
7147
- if (paths.length === 0)
7148
- return event;
7149
- const copy = structuredClone(event);
7150
- for (const path of paths) {
7151
- setPath(copy, path, replacement);
7152
- }
7153
- return copy;
7154
- }
7155
- function redactSensitiveKeys(event, replacement = "[REDACTED]") {
7156
- return redactValue2(event, replacement);
7157
- }
7158
- function shouldRedactKey(key) {
7159
- return /secret|token|password|api[_-]?key|authorization/i.test(key);
7160
- }
7161
- function redactValue2(value, replacement) {
7162
- if (Array.isArray(value))
7163
- return value.map((item) => redactValue2(item, replacement));
7164
- if (!value || typeof value !== "object")
7165
- return value;
7166
- return Object.fromEntries(Object.entries(value).map(([key, item]) => [
7167
- key,
7168
- shouldRedactKey(key) ? replacement : redactValue2(item, replacement)
7169
- ]));
7170
- }
7171
- function setPath(input, path, replacement) {
7172
- const parts = path.split(".");
7173
- let cursor = input;
7174
- for (const part of parts.slice(0, -1)) {
7175
- const next = cursor[part];
7176
- if (!next || typeof next !== "object")
7177
- return;
7178
- cursor = next;
7179
- }
7180
- const last = parts.at(-1);
7181
- if (last && last in cursor)
7182
- cursor[last] = replacement;
7415
+ function queryClientEvents(events, options) {
7416
+ let rows = events;
7417
+ if (options.eventId)
7418
+ rows = rows.filter((event) => event.id === options.eventId);
7419
+ if (options.source)
7420
+ rows = rows.filter((event) => event.source === options.source);
7421
+ if (options.type)
7422
+ rows = rows.filter((event) => event.type === options.type);
7423
+ if (options.cursor)
7424
+ rows = rows.slice(decodeLocalJsonEventCursor(options.cursor, options));
7425
+ if (options.limit !== undefined)
7426
+ rows = rows.slice(0, normalizeEventPageLimit(options.limit));
7427
+ return rows;
7183
7428
  }
7184
7429
  function normalizeTime(value) {
7185
7430
  if (!value)
@@ -7193,9 +7438,22 @@ function normalizeRetryPolicy(policy) {
7193
7438
  multiplier: Math.max(1, policy?.multiplier ?? 2)
7194
7439
  };
7195
7440
  }
7196
- var HASNA_EVENTS_DIR_ENV = "HASNA_EVENTS_DIR", HASNA_EVENTS_HOME_ENV = "HASNA_EVENTS_HOME", DEFAULT_SIGNATURE_TOLERANCE_MS;
7441
+ 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;
7197
7442
  var init_dist = __esm(() => {
7198
7443
  DEFAULT_SIGNATURE_TOLERANCE_MS = 5 * 60 * 1000;
7444
+ EventValidationError = class EventValidationError extends Error {
7445
+ eventType;
7446
+ issues;
7447
+ constructor(eventType, issues) {
7448
+ const detail = issues.map((issue) => `${issue.path || "<root>"}: ${issue.message}`).join("; ");
7449
+ super(`Event validation failed for type "${eventType}": ${detail}`);
7450
+ this.name = "EventValidationError";
7451
+ this.eventType = eventType;
7452
+ this.issues = issues;
7453
+ }
7454
+ };
7455
+ defaultEventTypeCatalog = new EventTypeCatalog;
7456
+ APP_EVENT_V1_MAX_DATA_BYTES = 32 * 1024;
7199
7457
  });
7200
7458
 
7201
7459
  // src/db/task-lists.ts
@@ -8070,8 +8328,12 @@ async function deliverWebhook(wh, event, body, attempt, db) {
8070
8328
  const sig = await crypto.subtle.sign("HMAC", key, encoder.encode(body));
8071
8329
  headers["X-Webhook-Signature"] = Array.from(new Uint8Array(sig)).map((b) => b.toString(16).padStart(2, "0")).join("");
8072
8330
  }
8073
- const resp = await fetch(wh.url, { method: "POST", headers, body });
8331
+ const resp = await fetch(wh.url, { method: "POST", headers, body, redirect: "manual" });
8074
8332
  const respText = await resp.text().catch(() => "");
8333
+ if (resp.status >= 300 && resp.status < 400) {
8334
+ logDelivery(db, wh.id, event, body, resp.status, "Blocked: webhook URL returned a 3xx redirect; redirects are never followed (SSRF prevention)", attempt);
8335
+ return;
8336
+ }
8075
8337
  logDelivery(db, wh.id, event, body, resp.status, respText.slice(0, 1000), attempt);
8076
8338
  if (resp.status >= 400 && attempt < MAX_RETRY_ATTEMPTS) {
8077
8339
  const delay = RETRY_BASE_DELAY_MS * Math.pow(2, attempt - 1);
@@ -12812,7 +13074,7 @@ import { createHash as createHash7 } from "crypto";
12812
13074
  // package.json
12813
13075
  var package_default = {
12814
13076
  name: "@hasna/todos",
12815
- version: "0.15.46",
13077
+ version: "0.15.47",
12816
13078
  description: "Universal task management for AI coding agents - CLI + MCP server + interactive TUI",
12817
13079
  type: "module",
12818
13080
  main: "dist/index.js",
@@ -12937,11 +13199,12 @@ var package_default = {
12937
13199
  commander: "^13.1.0",
12938
13200
  ink: "^5.2.0",
12939
13201
  react: "^18.3.1",
12940
- zod: "^3.24.2"
13202
+ zod: "3.25.76"
12941
13203
  },
12942
13204
  overrides: {
12943
13205
  ajv: "8.20.0",
12944
- "fast-uri": "3.1.2"
13206
+ "fast-uri": "3.1.2",
13207
+ zod: "3.25.76"
12945
13208
  },
12946
13209
  devDependencies: {
12947
13210
  "@types/bun": "^1.2.4",
@@ -13957,9 +14220,11 @@ class PostgresJsonRecordStore {
13957
14220
  async ensureSchema() {
13958
14221
  if (!this.schemaReady) {
13959
14222
  this.schemaReady = (async () => {
13960
- for (const sql of postgresTodosSyncSchemaSql(this.tableName, this.cursorTableName)) {
13961
- await this.options.client.query(sql);
13962
- }
14223
+ await retryOnTransientPostgresError(async () => {
14224
+ for (const sql of postgresTodosSyncSchemaSql(this.tableName, this.cursorTableName)) {
14225
+ await this.options.client.query(sql);
14226
+ }
14227
+ });
13963
14228
  })().catch((error) => {
13964
14229
  this.schemaReady = null;
13965
14230
  throw error;
@@ -14373,13 +14638,14 @@ class PostgresJsonRecordStore {
14373
14638
  throw new Error(divergentAuditHistoryReplayError(value.id));
14374
14639
  }
14375
14640
  async withTaskParentIntegrityTransaction(fn) {
14376
- if (typeof this.options.client.transaction !== "function") {
14641
+ const transaction = this.options.client.transaction;
14642
+ if (typeof transaction !== "function") {
14377
14643
  throw new Error("TASK_PARENT_ATOMICITY_UNAVAILABLE: PostgreSQL parent writes and task deletion require transaction(callback)");
14378
14644
  }
14379
- return this.options.client.transaction(async (client) => {
14645
+ return retryOnTransientPostgresError(() => transaction(async (client) => {
14380
14646
  await client.query("/* todos:task-parent-integrity-lock */ SELECT pg_advisory_xact_lock(hashtextextended($1 || ':task-parent-integrity', 0))", [this.service]);
14381
14647
  return fn(client);
14382
- });
14648
+ }));
14383
14649
  }
14384
14650
  async upsertTaskWithPlanMembershipGuard(value, guardedPlanIds, explicitProject, context = {}, parentGuard, queryClient) {
14385
14651
  const planIds = [...new Set(guardedPlanIds.filter(Boolean))].sort();
@@ -16265,6 +16531,41 @@ function compareClock(left, right) {
16265
16531
  function numberValue2(value) {
16266
16532
  return typeof value === "number" && Number.isSafeInteger(value) ? value : null;
16267
16533
  }
16534
+ var TRANSIENT_POSTGRES_SQLSTATES = new Set(["55P03", "40P01", "40001"]);
16535
+ var TRANSIENT_POSTGRES_MESSAGE_MARKERS = [
16536
+ "canceling statement due to lock timeout",
16537
+ "deadlock detected",
16538
+ "could not serialize access"
16539
+ ];
16540
+ function isTransientPostgresError(error) {
16541
+ if (typeof error !== "object" || error === null)
16542
+ return false;
16543
+ const candidate = error;
16544
+ const states = [candidate.code, candidate.errno, candidate.sqlState, candidate.sqlstate];
16545
+ if (typeof candidate.cause === "object" && candidate.cause !== null) {
16546
+ const cause = candidate.cause;
16547
+ states.push(cause.code, cause.errno, cause.sqlState, cause.sqlstate);
16548
+ }
16549
+ if (states.some((state) => typeof state === "string" && TRANSIENT_POSTGRES_SQLSTATES.has(state))) {
16550
+ return true;
16551
+ }
16552
+ const message = typeof candidate.message === "string" ? candidate.message : candidate.cause?.message;
16553
+ return typeof message === "string" && TRANSIENT_POSTGRES_MESSAGE_MARKERS.some((marker) => message.includes(marker));
16554
+ }
16555
+ async function retryOnTransientPostgresError(fn, attempts = 2, delayMs = 150) {
16556
+ let lastError;
16557
+ for (let attempt = 1;attempt <= attempts; attempt += 1) {
16558
+ try {
16559
+ return await fn();
16560
+ } catch (error) {
16561
+ lastError = error;
16562
+ if (!isTransientPostgresError(error) || attempt === attempts)
16563
+ throw error;
16564
+ await new Promise((resolve) => setTimeout(resolve, delayMs * attempt));
16565
+ }
16566
+ }
16567
+ throw lastError;
16568
+ }
16268
16569
  function isPostgresUniqueViolation(error) {
16269
16570
  if (typeof error !== "object" || error === null)
16270
16571
  return false;