@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/cli/index.js CHANGED
@@ -2123,7 +2123,7 @@ var package_default;
2123
2123
  var init_package = __esm(() => {
2124
2124
  package_default = {
2125
2125
  name: "@hasna/todos",
2126
- version: "0.15.46",
2126
+ version: "0.15.47",
2127
2127
  description: "Universal task management for AI coding agents - CLI + MCP server + interactive TUI",
2128
2128
  type: "module",
2129
2129
  main: "dist/index.js",
@@ -2248,11 +2248,12 @@ var init_package = __esm(() => {
2248
2248
  commander: "^13.1.0",
2249
2249
  ink: "^5.2.0",
2250
2250
  react: "^18.3.1",
2251
- zod: "^3.24.2"
2251
+ zod: "3.25.76"
2252
2252
  },
2253
2253
  overrides: {
2254
2254
  ajv: "8.20.0",
2255
- "fast-uri": "3.1.2"
2255
+ "fast-uri": "3.1.2",
2256
+ zod: "3.25.76"
2256
2257
  },
2257
2258
  devDependencies: {
2258
2259
  "@types/bun": "^1.2.4",
@@ -14578,8 +14579,9 @@ var init_event_hooks = __esm(() => {
14578
14579
  VALID_TARGETS = new Set(["stdout", "file", "socket", "script"]);
14579
14580
  });
14580
14581
 
14581
- // node_modules/.bun/@hasna+events@0.1.11/node_modules/@hasna/events/dist/index.js
14582
+ // node_modules/.bun/@hasna+events@0.1.16/node_modules/@hasna/events/dist/index.js
14582
14583
  import { chmod, mkdir, readFile, rename, writeFile } from "fs/promises";
14584
+ import { Buffer as Buffer2 } from "buffer";
14583
14585
  import { existsSync as existsSync6 } from "fs";
14584
14586
  import { homedir as homedir2 } from "os";
14585
14587
  import { join as join5 } from "path";
@@ -14688,11 +14690,13 @@ function getEventsDataDir(override) {
14688
14690
 
14689
14691
  class JsonEventsStore {
14690
14692
  dataDir;
14693
+ runtime;
14691
14694
  channelsPath;
14692
14695
  eventsPath;
14693
14696
  deliveriesPath;
14694
14697
  constructor(dataDir = getEventsDataDir()) {
14695
14698
  this.dataDir = dataDir;
14699
+ this.runtime = localJsonRuntime(dataDir);
14696
14700
  this.channelsPath = join5(dataDir, "channels.json");
14697
14701
  this.eventsPath = join5(dataDir, "events.json");
14698
14702
  this.deliveriesPath = join5(dataDir, "deliveries.json");
@@ -14740,13 +14744,58 @@ class JsonEventsStore {
14740
14744
  await this.writeJson(this.eventsPath, events);
14741
14745
  return event;
14742
14746
  }
14743
- async listEvents() {
14747
+ async appendEventOnce(event, options = {}) {
14744
14748
  await this.init();
14745
- return this.readJson(this.eventsPath, []);
14749
+ const events = await this.readJson(this.eventsPath, []);
14750
+ const dedupe2 = options.dedupe !== false;
14751
+ if (dedupe2) {
14752
+ const existing = findEventByIdentity(events, { id: event.id, dedupeKey: event.dedupeKey });
14753
+ if (existing) {
14754
+ return {
14755
+ event: existing,
14756
+ stored: false,
14757
+ deduped: true,
14758
+ identity: { id: existing.id, dedupeKey: existing.dedupeKey }
14759
+ };
14760
+ }
14761
+ }
14762
+ events.push(event);
14763
+ await this.writeJson(this.eventsPath, events);
14764
+ return {
14765
+ event,
14766
+ stored: true,
14767
+ deduped: false,
14768
+ identity: { id: event.id, dedupeKey: event.dedupeKey }
14769
+ };
14770
+ }
14771
+ async listEvents(options = {}) {
14772
+ await this.init();
14773
+ const events = await this.readJson(this.eventsPath, []);
14774
+ return queryEvents(events, options);
14775
+ }
14776
+ async listEventsPage(options = {}) {
14777
+ await this.init();
14778
+ const events = await this.readJson(this.eventsPath, []);
14779
+ const queried = queryEvents(events, {
14780
+ eventId: options.eventId,
14781
+ source: options.source,
14782
+ type: options.type
14783
+ });
14784
+ const offset = decodeLocalJsonEventCursor(options.cursor, options);
14785
+ const limit = normalizeEventPageLimit(options.limit);
14786
+ const pageEvents = queried.slice(offset, offset + limit);
14787
+ const nextOffset = offset + pageEvents.length;
14788
+ const hasMore = nextOffset < queried.length;
14789
+ return {
14790
+ events: pageEvents,
14791
+ cursor: options.cursor,
14792
+ nextCursor: hasMore ? encodeLocalJsonEventCursor(nextOffset, options) : undefined,
14793
+ hasMore
14794
+ };
14746
14795
  }
14747
14796
  async findEventByIdentity(identity) {
14748
14797
  const events = await this.listEvents();
14749
- return events.find((event) => identity.id !== undefined && event.id === identity.id || identity.dedupeKey !== undefined && event.dedupeKey === identity.dedupeKey);
14798
+ return findEventByIdentity(events, identity);
14750
14799
  }
14751
14800
  async appendDelivery(result) {
14752
14801
  await this.init();
@@ -14797,6 +14846,83 @@ class JsonEventsStore {
14797
14846
  });
14798
14847
  }
14799
14848
  }
14849
+ function localJsonRuntime(dataDir = getEventsDataDir()) {
14850
+ return {
14851
+ mode: "local-files",
14852
+ name: "json-events-store",
14853
+ remote: false,
14854
+ localFiles: true,
14855
+ localSqlite: false,
14856
+ postgres: false,
14857
+ s3: false,
14858
+ aws: false,
14859
+ durable: true,
14860
+ idempotency: "best-effort-local",
14861
+ replayCursors: true,
14862
+ description: `Local JSON files in ${dataDir}; no SQLite, Postgres, S3, or AWS runtime is configured by this store.`
14863
+ };
14864
+ }
14865
+ function encodeLocalJsonEventCursor(offset, options = {}) {
14866
+ if (!Number.isInteger(offset) || offset < 0)
14867
+ throw new Error(`Invalid event cursor offset: ${offset}`);
14868
+ const payload = {
14869
+ offset,
14870
+ eventId: options.eventId,
14871
+ source: options.source,
14872
+ type: options.type
14873
+ };
14874
+ return `${LOCAL_JSON_EVENT_CURSOR_PREFIX}${Buffer2.from(JSON.stringify(payload), "utf-8").toString("base64url")}`;
14875
+ }
14876
+ function decodeLocalJsonEventCursor(cursor, options = {}) {
14877
+ if (!cursor)
14878
+ return 0;
14879
+ if (!cursor.startsWith(LOCAL_JSON_EVENT_CURSOR_PREFIX))
14880
+ throw new Error(`Invalid local JSON event cursor: ${cursor}`);
14881
+ const rawPayload = cursor.slice(LOCAL_JSON_EVENT_CURSOR_PREFIX.length);
14882
+ let payload;
14883
+ try {
14884
+ payload = JSON.parse(Buffer2.from(rawPayload, "base64url").toString("utf-8"));
14885
+ } catch {
14886
+ throw new Error(`Invalid local JSON event cursor: ${cursor}`);
14887
+ }
14888
+ const offset = payload.offset;
14889
+ if (!Number.isInteger(offset) || offset < 0)
14890
+ throw new Error(`Invalid local JSON event cursor: ${cursor}`);
14891
+ assertCursorFilter("eventId", payload.eventId, options.eventId);
14892
+ assertCursorFilter("source", payload.source, options.source);
14893
+ assertCursorFilter("type", payload.type, options.type);
14894
+ return offset;
14895
+ }
14896
+ function normalizeEventPageLimit(limit) {
14897
+ if (limit === undefined)
14898
+ return DEFAULT_EVENT_PAGE_LIMIT;
14899
+ if (!Number.isInteger(limit) || limit < 1)
14900
+ throw new Error(`Event page limit must be a positive integer, got ${limit}`);
14901
+ return Math.min(limit, MAX_EVENT_PAGE_LIMIT);
14902
+ }
14903
+ function queryEvents(events, options) {
14904
+ let rows = events;
14905
+ if (options.eventId)
14906
+ rows = rows.filter((event) => event.id === options.eventId);
14907
+ if (options.source)
14908
+ rows = rows.filter((event) => event.source === options.source);
14909
+ if (options.type)
14910
+ rows = rows.filter((event) => event.type === options.type);
14911
+ if (options.cursor) {
14912
+ const offset = decodeLocalJsonEventCursor(options.cursor, options);
14913
+ rows = rows.slice(offset);
14914
+ }
14915
+ if (options.limit !== undefined)
14916
+ rows = rows.slice(0, normalizeEventPageLimit(options.limit));
14917
+ return rows;
14918
+ }
14919
+ function assertCursorFilter(name, cursorValue, optionValue) {
14920
+ if (cursorValue !== optionValue)
14921
+ throw new Error(`Local JSON event cursor ${name} filter mismatch`);
14922
+ }
14923
+ function findEventByIdentity(events, identity) {
14924
+ return events.find((event) => identity.id !== undefined && event.id === identity.id || identity.dedupeKey !== undefined && event.dedupeKey === identity.dedupeKey);
14925
+ }
14800
14926
  function buildSignatureBase(timestamp2, body) {
14801
14927
  return `${timestamp2}.${body}`;
14802
14928
  }
@@ -14810,21 +14936,27 @@ function now2() {
14810
14936
  function truncate(value, max = 4096) {
14811
14937
  return value.length > max ? `${value.slice(0, max)}...` : value;
14812
14938
  }
14813
- function buildWebhookRequest(event, channel) {
14939
+ function buildWebhookRequest(event, channel, options = {}) {
14814
14940
  if (!channel.webhook)
14815
14941
  throw new Error(`Channel ${channel.id} has no webhook config`);
14942
+ for (const name of Object.keys(channel.webhook.headers ?? {})) {
14943
+ if (/^x-hasna-/i.test(name)) {
14944
+ throw new Error(`Webhook header ${name} is reserved for signed delivery metadata`);
14945
+ }
14946
+ }
14816
14947
  const body = JSON.stringify(event);
14817
- const timestamp2 = event.time;
14948
+ const timestamp2 = options.timestamp ?? new Date().toISOString();
14818
14949
  const headers = {
14819
14950
  "Content-Type": "application/json",
14820
14951
  "User-Agent": "@hasna/events",
14821
14952
  "X-Hasna-Event-Id": event.id,
14822
14953
  "X-Hasna-Event-Type": event.type,
14823
- "X-Hasna-Timestamp": timestamp2,
14824
- ...channel.webhook.headers
14954
+ ...channel.webhook.headers,
14955
+ "X-Hasna-Timestamp": timestamp2
14825
14956
  };
14826
- if (channel.webhook.secret) {
14827
- headers["X-Hasna-Signature"] = signPayload(channel.webhook.secret, timestamp2, body);
14957
+ const secret = options.secret ?? channel.webhook.secret;
14958
+ if (secret) {
14959
+ headers["X-Hasna-Signature"] = signPayload(secret, timestamp2, body);
14828
14960
  }
14829
14961
  return { body, headers };
14830
14962
  }
@@ -14832,7 +14964,21 @@ async function dispatchWebhook(event, channel, options = {}) {
14832
14964
  if (!channel.webhook)
14833
14965
  throw new Error(`Channel ${channel.id} has no webhook config`);
14834
14966
  const startedAt = now2();
14835
- const { body, headers } = buildWebhookRequest(event, channel);
14967
+ let secret = channel.webhook.secret;
14968
+ if (channel.webhook.secretRef) {
14969
+ if (!options.secretResolver) {
14970
+ return failedAttempt(startedAt, "Webhook secret reference has no runtime resolver");
14971
+ }
14972
+ try {
14973
+ secret = await options.secretResolver(channel.webhook.secretRef);
14974
+ } catch {
14975
+ return failedAttempt(startedAt, "Webhook secret reference could not be resolved");
14976
+ }
14977
+ if (!secret)
14978
+ return failedAttempt(startedAt, "Webhook secret reference could not be resolved");
14979
+ }
14980
+ const timestamp2 = (options.now?.() ?? new Date).toISOString();
14981
+ const { body, headers } = buildWebhookRequest(event, channel, { secret, timestamp: timestamp2 });
14836
14982
  const controller = new AbortController;
14837
14983
  const timeout = setTimeout(() => controller.abort(), channel.webhook.timeoutMs ?? 15000);
14838
14984
  try {
@@ -14864,6 +15010,15 @@ async function dispatchWebhook(event, channel, options = {}) {
14864
15010
  clearTimeout(timeout);
14865
15011
  }
14866
15012
  }
15013
+ function failedAttempt(startedAt, error) {
15014
+ return {
15015
+ attempt: 1,
15016
+ status: "failed",
15017
+ startedAt,
15018
+ completedAt: now2(),
15019
+ error
15020
+ };
15021
+ }
14867
15022
  async function dispatchCommand(event, channel) {
14868
15023
  if (!channel.command)
14869
15024
  throw new Error(`Channel ${channel.id} has no command config`);
@@ -14952,6 +15107,76 @@ function createDeliveryResult(event, channel, attempts) {
14952
15107
  completedAt: attempts.at(-1)?.completedAt ?? now2()
14953
15108
  };
14954
15109
  }
15110
+
15111
+ class EventTypeCatalog {
15112
+ definitions = new Map;
15113
+ register(definition) {
15114
+ this.definitions.set(definition.type, definition);
15115
+ return this;
15116
+ }
15117
+ unregister(type) {
15118
+ return this.definitions.delete(type);
15119
+ }
15120
+ has(type) {
15121
+ return this.definitions.has(type);
15122
+ }
15123
+ get(type) {
15124
+ return this.definitions.get(type);
15125
+ }
15126
+ list() {
15127
+ return [...this.definitions.values()];
15128
+ }
15129
+ validateEvent(event) {
15130
+ const definition = this.definitions.get(event.type);
15131
+ if (!definition)
15132
+ return { ok: true };
15133
+ return definition.validate(event.data, event);
15134
+ }
15135
+ assertEventValid(event) {
15136
+ const result = this.validateEvent(event);
15137
+ if (!result.ok) {
15138
+ throw new EventValidationError(event.type, result.issues);
15139
+ }
15140
+ }
15141
+ }
15142
+ function redactPaths(event, paths, replacement = "[REDACTED]") {
15143
+ if (paths.length === 0)
15144
+ return event;
15145
+ const copy = structuredClone(event);
15146
+ for (const path of paths) {
15147
+ setPath(copy, path, replacement);
15148
+ }
15149
+ return copy;
15150
+ }
15151
+ function redactSensitiveKeys(event, replacement = "[REDACTED]") {
15152
+ return redactValue2(event, replacement);
15153
+ }
15154
+ function shouldRedactKey(key) {
15155
+ return /secret|token|password|api[_-]?key|authorization/i.test(key);
15156
+ }
15157
+ function redactValue2(value, replacement) {
15158
+ if (Array.isArray(value))
15159
+ return value.map((item) => redactValue2(item, replacement));
15160
+ if (!value || typeof value !== "object")
15161
+ return value;
15162
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [
15163
+ key,
15164
+ shouldRedactKey(key) ? replacement : redactValue2(item, replacement)
15165
+ ]));
15166
+ }
15167
+ function setPath(input, path, replacement) {
15168
+ const parts = path.split(".");
15169
+ let cursor = input;
15170
+ for (const part of parts.slice(0, -1)) {
15171
+ const next = cursor[part];
15172
+ if (!next || typeof next !== "object")
15173
+ return;
15174
+ cursor = next;
15175
+ }
15176
+ const last = parts.at(-1);
15177
+ if (last && last in cursor)
15178
+ cursor[last] = replacement;
15179
+ }
14955
15180
  function createEvent(input) {
14956
15181
  return {
14957
15182
  id: input.id ?? randomUUID22(),
@@ -14972,10 +15197,18 @@ class EventsClient {
14972
15197
  store;
14973
15198
  redactors;
14974
15199
  transportOptions;
15200
+ catalog;
15201
+ validateCatalogTypes;
14975
15202
  constructor(options = {}) {
14976
15203
  this.store = options.store ?? new JsonEventsStore(options.dataDir);
14977
15204
  this.redactors = options.redactors ?? [];
14978
- this.transportOptions = { fetchImpl: options.fetchImpl };
15205
+ this.transportOptions = {
15206
+ fetchImpl: options.fetchImpl,
15207
+ secretResolver: options.secretResolver,
15208
+ now: options.now
15209
+ };
15210
+ this.catalog = options.catalog ?? defaultEventTypeCatalog;
15211
+ this.validateCatalogTypes = options.validateCatalogTypes ?? false;
14979
15212
  }
14980
15213
  async addChannel(input) {
14981
15214
  const timestamp2 = new Date().toISOString();
@@ -14993,18 +15226,40 @@ class EventsClient {
14993
15226
  }
14994
15227
  async emit(input, options = {}) {
14995
15228
  const event = options.redactSensitiveData === false ? createEvent(input) : redactSensitiveKeys(createEvent(input));
14996
- if (options.dedupe !== false) {
14997
- const existing = await this.store.findEventByIdentity({ id: input.id, dedupeKey: event.dedupeKey });
14998
- if (existing) {
14999
- return { event: existing, deliveries: [], deduped: true };
15000
- }
15001
- }
15002
- await this.store.appendEvent(event);
15003
- const deliveries = options.deliver === false ? [] : await this.deliver(event);
15004
- return { event, deliveries, deduped: false };
15005
- }
15006
- async listEvents() {
15007
- return this.store.listEvents();
15229
+ if (options.validate ?? this.validateCatalogTypes) {
15230
+ this.catalog.assertEventValid(event);
15231
+ }
15232
+ const append = await this.appendEvent(event, { dedupe: options.dedupe !== false });
15233
+ if (append.deduped) {
15234
+ return { event: append.event, deliveries: [], deduped: true };
15235
+ }
15236
+ const deliveries = options.deliver === false ? [] : await this.deliver(append.event);
15237
+ return { event: append.event, deliveries, deduped: false };
15238
+ }
15239
+ async listEvents(options = {}) {
15240
+ if (Object.keys(options).length === 0)
15241
+ return this.store.listEvents();
15242
+ return queryClientEvents(await this.store.listEvents(), options);
15243
+ }
15244
+ async listEventsPage(options = {}) {
15245
+ if (this.store.listEventsPage)
15246
+ return this.store.listEventsPage(options);
15247
+ const events = queryClientEvents(await this.store.listEvents(), {
15248
+ eventId: options.eventId,
15249
+ source: options.source,
15250
+ type: options.type
15251
+ });
15252
+ const offset = decodeLocalJsonEventCursor(options.cursor, options);
15253
+ const limit = normalizeEventPageLimit(options.limit);
15254
+ const pageEvents = events.slice(offset, offset + limit);
15255
+ const nextOffset = offset + pageEvents.length;
15256
+ const hasMore = nextOffset < events.length;
15257
+ return {
15258
+ events: pageEvents,
15259
+ cursor: options.cursor,
15260
+ nextCursor: hasMore ? encodeLocalJsonEventCursor(nextOffset, options) : undefined,
15261
+ hasMore
15262
+ };
15008
15263
  }
15009
15264
  async listDeliveries() {
15010
15265
  return this.store.listDeliveries();
@@ -15072,22 +15327,37 @@ class EventsClient {
15072
15327
  return result;
15073
15328
  }
15074
15329
  async replay(options = {}) {
15075
- const events = (await this.store.listEvents()).filter((event) => {
15076
- if (options.eventId && event.id !== options.eventId)
15077
- return false;
15078
- if (options.source && event.source !== options.source)
15079
- return false;
15080
- if (options.type && event.type !== options.type)
15081
- return false;
15082
- return true;
15083
- });
15330
+ const page = options.cursor || options.limit !== undefined ? await this.listEventsPage(options) : { events: await this.listEvents(options), hasMore: false };
15084
15331
  if (options.dryRun)
15085
- return { events, deliveries: [] };
15332
+ return { events: page.events, deliveries: [], cursor: page.cursor, nextCursor: page.nextCursor, hasMore: page.hasMore };
15086
15333
  const deliveries = [];
15087
- for (const event of events) {
15334
+ for (const event of page.events) {
15088
15335
  deliveries.push(...await this.deliver(event));
15089
15336
  }
15090
- return { events, deliveries };
15337
+ return { events: page.events, deliveries, cursor: page.cursor, nextCursor: page.nextCursor, hasMore: page.hasMore };
15338
+ }
15339
+ async appendEvent(event, options) {
15340
+ if (this.store.appendEventOnce) {
15341
+ return this.store.appendEventOnce(event, { dedupe: options.dedupe });
15342
+ }
15343
+ if (options.dedupe) {
15344
+ const existing = await this.store.findEventByIdentity({ id: event.id, dedupeKey: event.dedupeKey });
15345
+ if (existing) {
15346
+ return {
15347
+ event: existing,
15348
+ stored: false,
15349
+ deduped: true,
15350
+ identity: { id: existing.id, dedupeKey: existing.dedupeKey }
15351
+ };
15352
+ }
15353
+ }
15354
+ const stored = await this.store.appendEvent(event);
15355
+ return {
15356
+ event: stored,
15357
+ stored: true,
15358
+ deduped: false,
15359
+ identity: { id: stored.id, dedupeKey: stored.dedupeKey }
15360
+ };
15091
15361
  }
15092
15362
  async applyRedaction(event, channel) {
15093
15363
  let next = redactPaths(event, channel.redact?.paths ?? [], channel.redact?.replacement ?? "[REDACTED]");
@@ -15114,43 +15384,19 @@ class EventsClient {
15114
15384
  return createDeliveryResult(event, channel, attempts);
15115
15385
  }
15116
15386
  }
15117
- function redactPaths(event, paths, replacement = "[REDACTED]") {
15118
- if (paths.length === 0)
15119
- return event;
15120
- const copy = structuredClone(event);
15121
- for (const path of paths) {
15122
- setPath(copy, path, replacement);
15123
- }
15124
- return copy;
15125
- }
15126
- function redactSensitiveKeys(event, replacement = "[REDACTED]") {
15127
- return redactValue2(event, replacement);
15128
- }
15129
- function shouldRedactKey(key) {
15130
- return /secret|token|password|api[_-]?key|authorization/i.test(key);
15131
- }
15132
- function redactValue2(value, replacement) {
15133
- if (Array.isArray(value))
15134
- return value.map((item) => redactValue2(item, replacement));
15135
- if (!value || typeof value !== "object")
15136
- return value;
15137
- return Object.fromEntries(Object.entries(value).map(([key, item]) => [
15138
- key,
15139
- shouldRedactKey(key) ? replacement : redactValue2(item, replacement)
15140
- ]));
15141
- }
15142
- function setPath(input, path, replacement) {
15143
- const parts = path.split(".");
15144
- let cursor = input;
15145
- for (const part of parts.slice(0, -1)) {
15146
- const next = cursor[part];
15147
- if (!next || typeof next !== "object")
15148
- return;
15149
- cursor = next;
15150
- }
15151
- const last = parts.at(-1);
15152
- if (last && last in cursor)
15153
- cursor[last] = replacement;
15387
+ function queryClientEvents(events, options) {
15388
+ let rows = events;
15389
+ if (options.eventId)
15390
+ rows = rows.filter((event) => event.id === options.eventId);
15391
+ if (options.source)
15392
+ rows = rows.filter((event) => event.source === options.source);
15393
+ if (options.type)
15394
+ rows = rows.filter((event) => event.type === options.type);
15395
+ if (options.cursor)
15396
+ rows = rows.slice(decodeLocalJsonEventCursor(options.cursor, options));
15397
+ if (options.limit !== undefined)
15398
+ rows = rows.slice(0, normalizeEventPageLimit(options.limit));
15399
+ return rows;
15154
15400
  }
15155
15401
  function normalizeTime(value) {
15156
15402
  if (!value)
@@ -15164,9 +15410,22 @@ function normalizeRetryPolicy(policy) {
15164
15410
  multiplier: Math.max(1, policy?.multiplier ?? 2)
15165
15411
  };
15166
15412
  }
15167
- var HASNA_EVENTS_DIR_ENV = "HASNA_EVENTS_DIR", HASNA_EVENTS_HOME_ENV = "HASNA_EVENTS_HOME", DEFAULT_SIGNATURE_TOLERANCE_MS;
15413
+ 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;
15168
15414
  var init_dist = __esm(() => {
15169
15415
  DEFAULT_SIGNATURE_TOLERANCE_MS = 5 * 60 * 1000;
15416
+ EventValidationError = class EventValidationError extends Error {
15417
+ eventType;
15418
+ issues;
15419
+ constructor(eventType, issues) {
15420
+ const detail = issues.map((issue) => `${issue.path || "<root>"}: ${issue.message}`).join("; ");
15421
+ super(`Event validation failed for type "${eventType}": ${detail}`);
15422
+ this.name = "EventValidationError";
15423
+ this.eventType = eventType;
15424
+ this.issues = issues;
15425
+ }
15426
+ };
15427
+ defaultEventTypeCatalog = new EventTypeCatalog;
15428
+ APP_EVENT_V1_MAX_DATA_BYTES = 32 * 1024;
15170
15429
  });
15171
15430
 
15172
15431
  // src/db/task-lists.ts
@@ -16221,8 +16480,12 @@ async function deliverWebhook(wh, event, body, attempt, db) {
16221
16480
  const sig = await crypto.subtle.sign("HMAC", key, encoder.encode(body));
16222
16481
  headers["X-Webhook-Signature"] = Array.from(new Uint8Array(sig)).map((b) => b.toString(16).padStart(2, "0")).join("");
16223
16482
  }
16224
- const resp = await fetch(wh.url, { method: "POST", headers, body });
16483
+ const resp = await fetch(wh.url, { method: "POST", headers, body, redirect: "manual" });
16225
16484
  const respText = await resp.text().catch(() => "");
16485
+ if (resp.status >= 300 && resp.status < 400) {
16486
+ logDelivery(db, wh.id, event, body, resp.status, "Blocked: webhook URL returned a 3xx redirect; redirects are never followed (SSRF prevention)", attempt);
16487
+ return;
16488
+ }
16226
16489
  logDelivery(db, wh.id, event, body, resp.status, respText.slice(0, 1000), attempt);
16227
16490
  if (resp.status >= 400 && attempt < MAX_RETRY_ATTEMPTS) {
16228
16491
  const delay = RETRY_BASE_DELAY_MS * Math.pow(2, attempt - 1);
@@ -32599,9 +32862,11 @@ class PostgresJsonRecordStore {
32599
32862
  async ensureSchema() {
32600
32863
  if (!this.schemaReady) {
32601
32864
  this.schemaReady = (async () => {
32602
- for (const sql of postgresTodosSyncSchemaSql(this.tableName, this.cursorTableName)) {
32603
- await this.options.client.query(sql);
32604
- }
32865
+ await retryOnTransientPostgresError(async () => {
32866
+ for (const sql of postgresTodosSyncSchemaSql(this.tableName, this.cursorTableName)) {
32867
+ await this.options.client.query(sql);
32868
+ }
32869
+ });
32605
32870
  })().catch((error) => {
32606
32871
  this.schemaReady = null;
32607
32872
  throw error;
@@ -33015,13 +33280,14 @@ class PostgresJsonRecordStore {
33015
33280
  throw new Error(divergentAuditHistoryReplayError(value.id));
33016
33281
  }
33017
33282
  async withTaskParentIntegrityTransaction(fn) {
33018
- if (typeof this.options.client.transaction !== "function") {
33283
+ const transaction = this.options.client.transaction;
33284
+ if (typeof transaction !== "function") {
33019
33285
  throw new Error("TASK_PARENT_ATOMICITY_UNAVAILABLE: PostgreSQL parent writes and task deletion require transaction(callback)");
33020
33286
  }
33021
- return this.options.client.transaction(async (client) => {
33287
+ return retryOnTransientPostgresError(() => transaction(async (client) => {
33022
33288
  await client.query("/* todos:task-parent-integrity-lock */ SELECT pg_advisory_xact_lock(hashtextextended($1 || ':task-parent-integrity', 0))", [this.service]);
33023
33289
  return fn(client);
33024
- });
33290
+ }));
33025
33291
  }
33026
33292
  async upsertTaskWithPlanMembershipGuard(value, guardedPlanIds, explicitProject, context = {}, parentGuard, queryClient) {
33027
33293
  const planIds = [...new Set(guardedPlanIds.filter(Boolean))].sort();
@@ -34904,6 +35170,35 @@ function compareClock(left, right) {
34904
35170
  function numberValue2(value) {
34905
35171
  return typeof value === "number" && Number.isSafeInteger(value) ? value : null;
34906
35172
  }
35173
+ function isTransientPostgresError(error) {
35174
+ if (typeof error !== "object" || error === null)
35175
+ return false;
35176
+ const candidate = error;
35177
+ const states = [candidate.code, candidate.errno, candidate.sqlState, candidate.sqlstate];
35178
+ if (typeof candidate.cause === "object" && candidate.cause !== null) {
35179
+ const cause = candidate.cause;
35180
+ states.push(cause.code, cause.errno, cause.sqlState, cause.sqlstate);
35181
+ }
35182
+ if (states.some((state) => typeof state === "string" && TRANSIENT_POSTGRES_SQLSTATES.has(state))) {
35183
+ return true;
35184
+ }
35185
+ const message = typeof candidate.message === "string" ? candidate.message : candidate.cause?.message;
35186
+ return typeof message === "string" && TRANSIENT_POSTGRES_MESSAGE_MARKERS.some((marker) => message.includes(marker));
35187
+ }
35188
+ async function retryOnTransientPostgresError(fn, attempts = 2, delayMs = 150) {
35189
+ let lastError;
35190
+ for (let attempt = 1;attempt <= attempts; attempt += 1) {
35191
+ try {
35192
+ return await fn();
35193
+ } catch (error) {
35194
+ lastError = error;
35195
+ if (!isTransientPostgresError(error) || attempt === attempts)
35196
+ throw error;
35197
+ await new Promise((resolve14) => setTimeout(resolve14, delayMs * attempt));
35198
+ }
35199
+ }
35200
+ throw lastError;
35201
+ }
34907
35202
  function isPostgresUniqueViolation(error) {
34908
35203
  if (typeof error !== "object" || error === null)
34909
35204
  return false;
@@ -34923,7 +35218,7 @@ function postgresConstraintName(error) {
34923
35218
  const constraint = candidate.constraint ?? candidate.constraint_name ?? cause?.constraint ?? cause?.constraint_name;
34924
35219
  return typeof constraint === "string" ? constraint : "";
34925
35220
  }
34926
- 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;
35221
+ 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;
34927
35222
  var init_postgres_adapter = __esm(() => {
34928
35223
  init_types();
34929
35224
  init_creator_identity();
@@ -34937,6 +35232,12 @@ var init_postgres_adapter = __esm(() => {
34937
35232
  init_audit_history_import();
34938
35233
  init_canonical();
34939
35234
  TASK_ORDER_BY = `ORDER BY ${TASK_ORDER_TIEBREAK}`;
35235
+ TRANSIENT_POSTGRES_SQLSTATES = new Set(["55P03", "40P01", "40001"]);
35236
+ TRANSIENT_POSTGRES_MESSAGE_MARKERS = [
35237
+ "canceling statement due to lock timeout",
35238
+ "deadlock detected",
35239
+ "could not serialize access"
35240
+ ];
34940
35241
  });
34941
35242
 
34942
35243
  // src/project-registration/postgres.ts