@hasna/todos 0.15.41 → 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.
Files changed (45) hide show
  1. package/dashboard/dist/assets/index-BNQ4gJua.js +342 -0
  2. package/dashboard/dist/assets/index-DjzvHUWt.css +1 -0
  3. package/dashboard/dist/index.html +2 -2
  4. package/dist/cli/cloud-router.d.ts.map +1 -1
  5. package/dist/cli/components/Dashboard.d.ts +1 -1
  6. package/dist/cli/components/Dashboard.d.ts.map +1 -1
  7. package/dist/cli/components/Header.d.ts +1 -1
  8. package/dist/cli/components/Header.d.ts.map +1 -1
  9. package/dist/cli/components/ProjectList.d.ts +1 -1
  10. package/dist/cli/components/ProjectList.d.ts.map +1 -1
  11. package/dist/cli/components/SearchView.d.ts +1 -1
  12. package/dist/cli/components/SearchView.d.ts.map +1 -1
  13. package/dist/cli/components/TaskDetail.d.ts +1 -1
  14. package/dist/cli/components/TaskDetail.d.ts.map +1 -1
  15. package/dist/cli/components/TaskForm.d.ts +1 -1
  16. package/dist/cli/components/TaskForm.d.ts.map +1 -1
  17. package/dist/cli/components/TaskList.d.ts +1 -1
  18. package/dist/cli/components/TaskList.d.ts.map +1 -1
  19. package/dist/cli/index.js +521 -120
  20. package/dist/contracts.js +346 -83
  21. package/dist/db/task-graph.d.ts.map +1 -1
  22. package/dist/db/task-lifecycle.d.ts.map +1 -1
  23. package/dist/db/webhooks.d.ts.map +1 -1
  24. package/dist/index.js +499 -113
  25. package/dist/lib/instant-compare.d.ts +35 -0
  26. package/dist/lib/instant-compare.d.ts.map +1 -0
  27. package/dist/mcp/index.js +520 -119
  28. package/dist/mcp.js +5 -4
  29. package/dist/pr-groups/postgres.d.ts.map +1 -1
  30. package/dist/project-registration/postgres.d.ts.map +1 -1
  31. package/dist/project-registration.js +466 -100
  32. package/dist/registry.js +346 -83
  33. package/dist/release-provenance.json +5 -5
  34. package/dist/server/cloud.d.ts.map +1 -1
  35. package/dist/server/index.js +1179 -309
  36. package/dist/storage/local-sqlite.d.ts.map +1 -1
  37. package/dist/storage/postgres-adapter.d.ts.map +1 -1
  38. package/dist/storage.js +447 -89
  39. package/dist/task-manifest/postgres.d.ts.map +1 -1
  40. package/dist/task-manifest.js +15 -6
  41. package/dist/task-subtree-transfer/postgres.d.ts.map +1 -1
  42. package/dist/task-subtree-transfer.js +15 -6
  43. package/package.json +5 -4
  44. package/dashboard/dist/assets/index-DJm6m6Yy.css +0 -1
  45. package/dashboard/dist/assets/index-DVotjwab.js +0 -346
package/dist/storage.js CHANGED
@@ -6194,8 +6194,9 @@ var init_event_hooks = __esm(() => {
6194
6194
  VALID_TARGETS = new Set(["stdout", "file", "socket", "script"]);
6195
6195
  });
6196
6196
 
6197
- // node_modules/.bun/@hasna+events@0.1.11/node_modules/@hasna/events/dist/index.js
6197
+ // node_modules/.bun/@hasna+events@0.1.16/node_modules/@hasna/events/dist/index.js
6198
6198
  import { chmod, mkdir, readFile, rename, writeFile } from "fs/promises";
6199
+ import { Buffer as Buffer2 } from "buffer";
6199
6200
  import { existsSync as existsSync5 } from "fs";
6200
6201
  import { homedir as homedir2 } from "os";
6201
6202
  import { join as join4 } from "path";
@@ -6304,11 +6305,13 @@ function getEventsDataDir(override) {
6304
6305
 
6305
6306
  class JsonEventsStore {
6306
6307
  dataDir;
6308
+ runtime;
6307
6309
  channelsPath;
6308
6310
  eventsPath;
6309
6311
  deliveriesPath;
6310
6312
  constructor(dataDir = getEventsDataDir()) {
6311
6313
  this.dataDir = dataDir;
6314
+ this.runtime = localJsonRuntime(dataDir);
6312
6315
  this.channelsPath = join4(dataDir, "channels.json");
6313
6316
  this.eventsPath = join4(dataDir, "events.json");
6314
6317
  this.deliveriesPath = join4(dataDir, "deliveries.json");
@@ -6356,13 +6359,58 @@ class JsonEventsStore {
6356
6359
  await this.writeJson(this.eventsPath, events);
6357
6360
  return event;
6358
6361
  }
6359
- async listEvents() {
6362
+ async appendEventOnce(event, options = {}) {
6360
6363
  await this.init();
6361
- return this.readJson(this.eventsPath, []);
6364
+ const events = await this.readJson(this.eventsPath, []);
6365
+ const dedupe = options.dedupe !== false;
6366
+ if (dedupe) {
6367
+ const existing = findEventByIdentity(events, { id: event.id, dedupeKey: event.dedupeKey });
6368
+ if (existing) {
6369
+ return {
6370
+ event: existing,
6371
+ stored: false,
6372
+ deduped: true,
6373
+ identity: { id: existing.id, dedupeKey: existing.dedupeKey }
6374
+ };
6375
+ }
6376
+ }
6377
+ events.push(event);
6378
+ await this.writeJson(this.eventsPath, events);
6379
+ return {
6380
+ event,
6381
+ stored: true,
6382
+ deduped: false,
6383
+ identity: { id: event.id, dedupeKey: event.dedupeKey }
6384
+ };
6385
+ }
6386
+ async listEvents(options = {}) {
6387
+ await this.init();
6388
+ const events = await this.readJson(this.eventsPath, []);
6389
+ return queryEvents(events, options);
6390
+ }
6391
+ async listEventsPage(options = {}) {
6392
+ await this.init();
6393
+ const events = await this.readJson(this.eventsPath, []);
6394
+ const queried = queryEvents(events, {
6395
+ eventId: options.eventId,
6396
+ source: options.source,
6397
+ type: options.type
6398
+ });
6399
+ const offset = decodeLocalJsonEventCursor(options.cursor, options);
6400
+ const limit = normalizeEventPageLimit(options.limit);
6401
+ const pageEvents = queried.slice(offset, offset + limit);
6402
+ const nextOffset = offset + pageEvents.length;
6403
+ const hasMore = nextOffset < queried.length;
6404
+ return {
6405
+ events: pageEvents,
6406
+ cursor: options.cursor,
6407
+ nextCursor: hasMore ? encodeLocalJsonEventCursor(nextOffset, options) : undefined,
6408
+ hasMore
6409
+ };
6362
6410
  }
6363
6411
  async findEventByIdentity(identity) {
6364
6412
  const events = await this.listEvents();
6365
- return events.find((event) => identity.id !== undefined && event.id === identity.id || identity.dedupeKey !== undefined && event.dedupeKey === identity.dedupeKey);
6413
+ return findEventByIdentity(events, identity);
6366
6414
  }
6367
6415
  async appendDelivery(result) {
6368
6416
  await this.init();
@@ -6413,6 +6461,83 @@ class JsonEventsStore {
6413
6461
  });
6414
6462
  }
6415
6463
  }
6464
+ function localJsonRuntime(dataDir = getEventsDataDir()) {
6465
+ return {
6466
+ mode: "local-files",
6467
+ name: "json-events-store",
6468
+ remote: false,
6469
+ localFiles: true,
6470
+ localSqlite: false,
6471
+ postgres: false,
6472
+ s3: false,
6473
+ aws: false,
6474
+ durable: true,
6475
+ idempotency: "best-effort-local",
6476
+ replayCursors: true,
6477
+ description: `Local JSON files in ${dataDir}; no SQLite, Postgres, S3, or AWS runtime is configured by this store.`
6478
+ };
6479
+ }
6480
+ function encodeLocalJsonEventCursor(offset, options = {}) {
6481
+ if (!Number.isInteger(offset) || offset < 0)
6482
+ throw new Error(`Invalid event cursor offset: ${offset}`);
6483
+ const payload = {
6484
+ offset,
6485
+ eventId: options.eventId,
6486
+ source: options.source,
6487
+ type: options.type
6488
+ };
6489
+ return `${LOCAL_JSON_EVENT_CURSOR_PREFIX}${Buffer2.from(JSON.stringify(payload), "utf-8").toString("base64url")}`;
6490
+ }
6491
+ function decodeLocalJsonEventCursor(cursor, options = {}) {
6492
+ if (!cursor)
6493
+ return 0;
6494
+ if (!cursor.startsWith(LOCAL_JSON_EVENT_CURSOR_PREFIX))
6495
+ throw new Error(`Invalid local JSON event cursor: ${cursor}`);
6496
+ const rawPayload = cursor.slice(LOCAL_JSON_EVENT_CURSOR_PREFIX.length);
6497
+ let payload;
6498
+ try {
6499
+ payload = JSON.parse(Buffer2.from(rawPayload, "base64url").toString("utf-8"));
6500
+ } catch {
6501
+ throw new Error(`Invalid local JSON event cursor: ${cursor}`);
6502
+ }
6503
+ const offset = payload.offset;
6504
+ if (!Number.isInteger(offset) || offset < 0)
6505
+ throw new Error(`Invalid local JSON event cursor: ${cursor}`);
6506
+ assertCursorFilter("eventId", payload.eventId, options.eventId);
6507
+ assertCursorFilter("source", payload.source, options.source);
6508
+ assertCursorFilter("type", payload.type, options.type);
6509
+ return offset;
6510
+ }
6511
+ function normalizeEventPageLimit(limit) {
6512
+ if (limit === undefined)
6513
+ return DEFAULT_EVENT_PAGE_LIMIT;
6514
+ if (!Number.isInteger(limit) || limit < 1)
6515
+ throw new Error(`Event page limit must be a positive integer, got ${limit}`);
6516
+ return Math.min(limit, MAX_EVENT_PAGE_LIMIT);
6517
+ }
6518
+ function queryEvents(events, options) {
6519
+ let rows = events;
6520
+ if (options.eventId)
6521
+ rows = rows.filter((event) => event.id === options.eventId);
6522
+ if (options.source)
6523
+ rows = rows.filter((event) => event.source === options.source);
6524
+ if (options.type)
6525
+ rows = rows.filter((event) => event.type === options.type);
6526
+ if (options.cursor) {
6527
+ const offset = decodeLocalJsonEventCursor(options.cursor, options);
6528
+ rows = rows.slice(offset);
6529
+ }
6530
+ if (options.limit !== undefined)
6531
+ rows = rows.slice(0, normalizeEventPageLimit(options.limit));
6532
+ return rows;
6533
+ }
6534
+ function assertCursorFilter(name, cursorValue, optionValue) {
6535
+ if (cursorValue !== optionValue)
6536
+ throw new Error(`Local JSON event cursor ${name} filter mismatch`);
6537
+ }
6538
+ function findEventByIdentity(events, identity) {
6539
+ return events.find((event) => identity.id !== undefined && event.id === identity.id || identity.dedupeKey !== undefined && event.dedupeKey === identity.dedupeKey);
6540
+ }
6416
6541
  function buildSignatureBase(timestamp2, body) {
6417
6542
  return `${timestamp2}.${body}`;
6418
6543
  }
@@ -6426,21 +6551,27 @@ function now2() {
6426
6551
  function truncate(value, max = 4096) {
6427
6552
  return value.length > max ? `${value.slice(0, max)}...` : value;
6428
6553
  }
6429
- function buildWebhookRequest(event, channel) {
6554
+ function buildWebhookRequest(event, channel, options = {}) {
6430
6555
  if (!channel.webhook)
6431
6556
  throw new Error(`Channel ${channel.id} has no webhook config`);
6557
+ for (const name of Object.keys(channel.webhook.headers ?? {})) {
6558
+ if (/^x-hasna-/i.test(name)) {
6559
+ throw new Error(`Webhook header ${name} is reserved for signed delivery metadata`);
6560
+ }
6561
+ }
6432
6562
  const body = JSON.stringify(event);
6433
- const timestamp2 = event.time;
6563
+ const timestamp2 = options.timestamp ?? new Date().toISOString();
6434
6564
  const headers = {
6435
6565
  "Content-Type": "application/json",
6436
6566
  "User-Agent": "@hasna/events",
6437
6567
  "X-Hasna-Event-Id": event.id,
6438
6568
  "X-Hasna-Event-Type": event.type,
6439
- "X-Hasna-Timestamp": timestamp2,
6440
- ...channel.webhook.headers
6569
+ ...channel.webhook.headers,
6570
+ "X-Hasna-Timestamp": timestamp2
6441
6571
  };
6442
- if (channel.webhook.secret) {
6443
- headers["X-Hasna-Signature"] = signPayload(channel.webhook.secret, timestamp2, body);
6572
+ const secret = options.secret ?? channel.webhook.secret;
6573
+ if (secret) {
6574
+ headers["X-Hasna-Signature"] = signPayload(secret, timestamp2, body);
6444
6575
  }
6445
6576
  return { body, headers };
6446
6577
  }
@@ -6448,7 +6579,21 @@ async function dispatchWebhook(event, channel, options = {}) {
6448
6579
  if (!channel.webhook)
6449
6580
  throw new Error(`Channel ${channel.id} has no webhook config`);
6450
6581
  const startedAt = now2();
6451
- const { body, headers } = buildWebhookRequest(event, channel);
6582
+ let secret = channel.webhook.secret;
6583
+ if (channel.webhook.secretRef) {
6584
+ if (!options.secretResolver) {
6585
+ return failedAttempt(startedAt, "Webhook secret reference has no runtime resolver");
6586
+ }
6587
+ try {
6588
+ secret = await options.secretResolver(channel.webhook.secretRef);
6589
+ } catch {
6590
+ return failedAttempt(startedAt, "Webhook secret reference could not be resolved");
6591
+ }
6592
+ if (!secret)
6593
+ return failedAttempt(startedAt, "Webhook secret reference could not be resolved");
6594
+ }
6595
+ const timestamp2 = (options.now?.() ?? new Date).toISOString();
6596
+ const { body, headers } = buildWebhookRequest(event, channel, { secret, timestamp: timestamp2 });
6452
6597
  const controller = new AbortController;
6453
6598
  const timeout = setTimeout(() => controller.abort(), channel.webhook.timeoutMs ?? 15000);
6454
6599
  try {
@@ -6480,6 +6625,15 @@ async function dispatchWebhook(event, channel, options = {}) {
6480
6625
  clearTimeout(timeout);
6481
6626
  }
6482
6627
  }
6628
+ function failedAttempt(startedAt, error) {
6629
+ return {
6630
+ attempt: 1,
6631
+ status: "failed",
6632
+ startedAt,
6633
+ completedAt: now2(),
6634
+ error
6635
+ };
6636
+ }
6483
6637
  async function dispatchCommand(event, channel) {
6484
6638
  if (!channel.command)
6485
6639
  throw new Error(`Channel ${channel.id} has no command config`);
@@ -6568,6 +6722,76 @@ function createDeliveryResult(event, channel, attempts) {
6568
6722
  completedAt: attempts.at(-1)?.completedAt ?? now2()
6569
6723
  };
6570
6724
  }
6725
+
6726
+ class EventTypeCatalog {
6727
+ definitions = new Map;
6728
+ register(definition) {
6729
+ this.definitions.set(definition.type, definition);
6730
+ return this;
6731
+ }
6732
+ unregister(type) {
6733
+ return this.definitions.delete(type);
6734
+ }
6735
+ has(type) {
6736
+ return this.definitions.has(type);
6737
+ }
6738
+ get(type) {
6739
+ return this.definitions.get(type);
6740
+ }
6741
+ list() {
6742
+ return [...this.definitions.values()];
6743
+ }
6744
+ validateEvent(event) {
6745
+ const definition = this.definitions.get(event.type);
6746
+ if (!definition)
6747
+ return { ok: true };
6748
+ return definition.validate(event.data, event);
6749
+ }
6750
+ assertEventValid(event) {
6751
+ const result = this.validateEvent(event);
6752
+ if (!result.ok) {
6753
+ throw new EventValidationError(event.type, result.issues);
6754
+ }
6755
+ }
6756
+ }
6757
+ function redactPaths(event, paths, replacement = "[REDACTED]") {
6758
+ if (paths.length === 0)
6759
+ return event;
6760
+ const copy = structuredClone(event);
6761
+ for (const path of paths) {
6762
+ setPath(copy, path, replacement);
6763
+ }
6764
+ return copy;
6765
+ }
6766
+ function redactSensitiveKeys(event, replacement = "[REDACTED]") {
6767
+ return redactValue2(event, replacement);
6768
+ }
6769
+ function shouldRedactKey(key) {
6770
+ return /secret|token|password|api[_-]?key|authorization/i.test(key);
6771
+ }
6772
+ function redactValue2(value, replacement) {
6773
+ if (Array.isArray(value))
6774
+ return value.map((item) => redactValue2(item, replacement));
6775
+ if (!value || typeof value !== "object")
6776
+ return value;
6777
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [
6778
+ key,
6779
+ shouldRedactKey(key) ? replacement : redactValue2(item, replacement)
6780
+ ]));
6781
+ }
6782
+ function setPath(input, path, replacement) {
6783
+ const parts = path.split(".");
6784
+ let cursor = input;
6785
+ for (const part of parts.slice(0, -1)) {
6786
+ const next = cursor[part];
6787
+ if (!next || typeof next !== "object")
6788
+ return;
6789
+ cursor = next;
6790
+ }
6791
+ const last = parts.at(-1);
6792
+ if (last && last in cursor)
6793
+ cursor[last] = replacement;
6794
+ }
6571
6795
  function createEvent(input) {
6572
6796
  return {
6573
6797
  id: input.id ?? randomUUID22(),
@@ -6588,10 +6812,18 @@ class EventsClient {
6588
6812
  store;
6589
6813
  redactors;
6590
6814
  transportOptions;
6815
+ catalog;
6816
+ validateCatalogTypes;
6591
6817
  constructor(options = {}) {
6592
6818
  this.store = options.store ?? new JsonEventsStore(options.dataDir);
6593
6819
  this.redactors = options.redactors ?? [];
6594
- this.transportOptions = { fetchImpl: options.fetchImpl };
6820
+ this.transportOptions = {
6821
+ fetchImpl: options.fetchImpl,
6822
+ secretResolver: options.secretResolver,
6823
+ now: options.now
6824
+ };
6825
+ this.catalog = options.catalog ?? defaultEventTypeCatalog;
6826
+ this.validateCatalogTypes = options.validateCatalogTypes ?? false;
6595
6827
  }
6596
6828
  async addChannel(input) {
6597
6829
  const timestamp2 = new Date().toISOString();
@@ -6609,18 +6841,40 @@ class EventsClient {
6609
6841
  }
6610
6842
  async emit(input, options = {}) {
6611
6843
  const event = options.redactSensitiveData === false ? createEvent(input) : redactSensitiveKeys(createEvent(input));
6612
- if (options.dedupe !== false) {
6613
- const existing = await this.store.findEventByIdentity({ id: input.id, dedupeKey: event.dedupeKey });
6614
- if (existing) {
6615
- return { event: existing, deliveries: [], deduped: true };
6616
- }
6617
- }
6618
- await this.store.appendEvent(event);
6619
- const deliveries = options.deliver === false ? [] : await this.deliver(event);
6620
- return { event, deliveries, deduped: false };
6621
- }
6622
- async listEvents() {
6623
- return this.store.listEvents();
6844
+ if (options.validate ?? this.validateCatalogTypes) {
6845
+ this.catalog.assertEventValid(event);
6846
+ }
6847
+ const append = await this.appendEvent(event, { dedupe: options.dedupe !== false });
6848
+ if (append.deduped) {
6849
+ return { event: append.event, deliveries: [], deduped: true };
6850
+ }
6851
+ const deliveries = options.deliver === false ? [] : await this.deliver(append.event);
6852
+ return { event: append.event, deliveries, deduped: false };
6853
+ }
6854
+ async listEvents(options = {}) {
6855
+ if (Object.keys(options).length === 0)
6856
+ return this.store.listEvents();
6857
+ return queryClientEvents(await this.store.listEvents(), options);
6858
+ }
6859
+ async listEventsPage(options = {}) {
6860
+ if (this.store.listEventsPage)
6861
+ return this.store.listEventsPage(options);
6862
+ const events = queryClientEvents(await this.store.listEvents(), {
6863
+ eventId: options.eventId,
6864
+ source: options.source,
6865
+ type: options.type
6866
+ });
6867
+ const offset = decodeLocalJsonEventCursor(options.cursor, options);
6868
+ const limit = normalizeEventPageLimit(options.limit);
6869
+ const pageEvents = events.slice(offset, offset + limit);
6870
+ const nextOffset = offset + pageEvents.length;
6871
+ const hasMore = nextOffset < events.length;
6872
+ return {
6873
+ events: pageEvents,
6874
+ cursor: options.cursor,
6875
+ nextCursor: hasMore ? encodeLocalJsonEventCursor(nextOffset, options) : undefined,
6876
+ hasMore
6877
+ };
6624
6878
  }
6625
6879
  async listDeliveries() {
6626
6880
  return this.store.listDeliveries();
@@ -6688,22 +6942,37 @@ class EventsClient {
6688
6942
  return result;
6689
6943
  }
6690
6944
  async replay(options = {}) {
6691
- const events = (await this.store.listEvents()).filter((event) => {
6692
- if (options.eventId && event.id !== options.eventId)
6693
- return false;
6694
- if (options.source && event.source !== options.source)
6695
- return false;
6696
- if (options.type && event.type !== options.type)
6697
- return false;
6698
- return true;
6699
- });
6945
+ const page = options.cursor || options.limit !== undefined ? await this.listEventsPage(options) : { events: await this.listEvents(options), hasMore: false };
6700
6946
  if (options.dryRun)
6701
- return { events, deliveries: [] };
6947
+ return { events: page.events, deliveries: [], cursor: page.cursor, nextCursor: page.nextCursor, hasMore: page.hasMore };
6702
6948
  const deliveries = [];
6703
- for (const event of events) {
6949
+ for (const event of page.events) {
6704
6950
  deliveries.push(...await this.deliver(event));
6705
6951
  }
6706
- return { events, deliveries };
6952
+ return { events: page.events, deliveries, cursor: page.cursor, nextCursor: page.nextCursor, hasMore: page.hasMore };
6953
+ }
6954
+ async appendEvent(event, options) {
6955
+ if (this.store.appendEventOnce) {
6956
+ return this.store.appendEventOnce(event, { dedupe: options.dedupe });
6957
+ }
6958
+ if (options.dedupe) {
6959
+ const existing = await this.store.findEventByIdentity({ id: event.id, dedupeKey: event.dedupeKey });
6960
+ if (existing) {
6961
+ return {
6962
+ event: existing,
6963
+ stored: false,
6964
+ deduped: true,
6965
+ identity: { id: existing.id, dedupeKey: existing.dedupeKey }
6966
+ };
6967
+ }
6968
+ }
6969
+ const stored = await this.store.appendEvent(event);
6970
+ return {
6971
+ event: stored,
6972
+ stored: true,
6973
+ deduped: false,
6974
+ identity: { id: stored.id, dedupeKey: stored.dedupeKey }
6975
+ };
6707
6976
  }
6708
6977
  async applyRedaction(event, channel) {
6709
6978
  let next = redactPaths(event, channel.redact?.paths ?? [], channel.redact?.replacement ?? "[REDACTED]");
@@ -6730,43 +6999,19 @@ class EventsClient {
6730
6999
  return createDeliveryResult(event, channel, attempts);
6731
7000
  }
6732
7001
  }
6733
- function redactPaths(event, paths, replacement = "[REDACTED]") {
6734
- if (paths.length === 0)
6735
- return event;
6736
- const copy = structuredClone(event);
6737
- for (const path of paths) {
6738
- setPath(copy, path, replacement);
6739
- }
6740
- return copy;
6741
- }
6742
- function redactSensitiveKeys(event, replacement = "[REDACTED]") {
6743
- return redactValue2(event, replacement);
6744
- }
6745
- function shouldRedactKey(key) {
6746
- return /secret|token|password|api[_-]?key|authorization/i.test(key);
6747
- }
6748
- function redactValue2(value, replacement) {
6749
- if (Array.isArray(value))
6750
- return value.map((item) => redactValue2(item, replacement));
6751
- if (!value || typeof value !== "object")
6752
- return value;
6753
- return Object.fromEntries(Object.entries(value).map(([key, item]) => [
6754
- key,
6755
- shouldRedactKey(key) ? replacement : redactValue2(item, replacement)
6756
- ]));
6757
- }
6758
- function setPath(input, path, replacement) {
6759
- const parts = path.split(".");
6760
- let cursor = input;
6761
- for (const part of parts.slice(0, -1)) {
6762
- const next = cursor[part];
6763
- if (!next || typeof next !== "object")
6764
- return;
6765
- cursor = next;
6766
- }
6767
- const last = parts.at(-1);
6768
- if (last && last in cursor)
6769
- cursor[last] = replacement;
7002
+ function queryClientEvents(events, options) {
7003
+ let rows = events;
7004
+ if (options.eventId)
7005
+ rows = rows.filter((event) => event.id === options.eventId);
7006
+ if (options.source)
7007
+ rows = rows.filter((event) => event.source === options.source);
7008
+ if (options.type)
7009
+ rows = rows.filter((event) => event.type === options.type);
7010
+ if (options.cursor)
7011
+ rows = rows.slice(decodeLocalJsonEventCursor(options.cursor, options));
7012
+ if (options.limit !== undefined)
7013
+ rows = rows.slice(0, normalizeEventPageLimit(options.limit));
7014
+ return rows;
6770
7015
  }
6771
7016
  function normalizeTime(value) {
6772
7017
  if (!value)
@@ -6780,9 +7025,22 @@ function normalizeRetryPolicy(policy) {
6780
7025
  multiplier: Math.max(1, policy?.multiplier ?? 2)
6781
7026
  };
6782
7027
  }
6783
- var HASNA_EVENTS_DIR_ENV = "HASNA_EVENTS_DIR", HASNA_EVENTS_HOME_ENV = "HASNA_EVENTS_HOME", DEFAULT_SIGNATURE_TOLERANCE_MS;
7028
+ 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;
6784
7029
  var init_dist = __esm(() => {
6785
7030
  DEFAULT_SIGNATURE_TOLERANCE_MS = 5 * 60 * 1000;
7031
+ EventValidationError = class EventValidationError extends Error {
7032
+ eventType;
7033
+ issues;
7034
+ constructor(eventType, issues) {
7035
+ const detail = issues.map((issue) => `${issue.path || "<root>"}: ${issue.message}`).join("; ");
7036
+ super(`Event validation failed for type "${eventType}": ${detail}`);
7037
+ this.name = "EventValidationError";
7038
+ this.eventType = eventType;
7039
+ this.issues = issues;
7040
+ }
7041
+ };
7042
+ defaultEventTypeCatalog = new EventTypeCatalog;
7043
+ APP_EVENT_V1_MAX_DATA_BYTES = 32 * 1024;
6786
7044
  });
6787
7045
 
6788
7046
  // src/db/task-lists.ts
@@ -7886,8 +8144,12 @@ async function deliverWebhook(wh, event, body, attempt, db) {
7886
8144
  const sig = await crypto.subtle.sign("HMAC", key, encoder.encode(body));
7887
8145
  headers["X-Webhook-Signature"] = Array.from(new Uint8Array(sig)).map((b) => b.toString(16).padStart(2, "0")).join("");
7888
8146
  }
7889
- const resp = await fetch(wh.url, { method: "POST", headers, body });
8147
+ const resp = await fetch(wh.url, { method: "POST", headers, body, redirect: "manual" });
7890
8148
  const respText = await resp.text().catch(() => "");
8149
+ if (resp.status >= 300 && resp.status < 400) {
8150
+ logDelivery(db, wh.id, event, body, resp.status, "Blocked: webhook URL returned a 3xx redirect; redirects are never followed (SSRF prevention)", attempt);
8151
+ return;
8152
+ }
7891
8153
  logDelivery(db, wh.id, event, body, resp.status, respText.slice(0, 1000), attempt);
7892
8154
  if (resp.status >= 400 && attempt < MAX_RETRY_ATTEMPTS) {
7893
8155
  const delay = RETRY_BASE_DELAY_MS * Math.pow(2, attempt - 1);
@@ -8651,7 +8913,7 @@ function getTaskGraph(taskId, direction = "both", db) {
8651
8913
  const deps = getTaskDependencies(t.id, d);
8652
8914
  const hasUnfinishedDeps = deps.some((dep) => {
8653
8915
  const depTask = getTask(dep.depends_on, d);
8654
- return depTask && depTask.status !== "completed";
8916
+ return depTask && isBlockingDependencyStatus(depTask.status);
8655
8917
  });
8656
8918
  return { id: t.id, short_id: t.short_id, title: t.title, status: t.status, priority: t.priority, is_blocked: hasUnfinishedDeps };
8657
8919
  }
@@ -8911,7 +9173,7 @@ function getBlockingDeps(id, db) {
8911
9173
  const blocking = [];
8912
9174
  for (const dep of deps) {
8913
9175
  const task = getTask(dep.depends_on, d);
8914
- if (task && task.status !== "completed")
9176
+ if (task && isBlockingDependencyStatus(task.status))
8915
9177
  blocking.push(task);
8916
9178
  }
8917
9179
  return blocking;
@@ -9235,7 +9497,7 @@ function getNextTask(agentId, filters, db) {
9235
9497
  conditions.push(`id IN (SELECT task_id FROM task_tags WHERE tag IN (${placeholders}))`);
9236
9498
  params.push(...filters.tags);
9237
9499
  }
9238
- conditions.push("id NOT IN (SELECT td.task_id FROM task_dependencies td JOIN tasks dep ON dep.id = td.depends_on WHERE dep.status != 'completed')");
9500
+ conditions.push("id NOT IN (SELECT td.task_id FROM task_dependencies td JOIN tasks dep ON dep.id = td.depends_on WHERE dep.status NOT IN ('completed', 'cancelled'))");
9239
9501
  const where = conditions.join(" AND ");
9240
9502
  let recentProjectIds = [];
9241
9503
  const assignedAliasParams = [];
@@ -9279,7 +9541,7 @@ function getActiveWork(filters, db) {
9279
9541
  }
9280
9542
  function getTasksChangedSince(since, filters, db) {
9281
9543
  const d = db || getDatabase();
9282
- const conditions = ["updated_at > ?"];
9544
+ const conditions = ["(julianday(updated_at) IS NULL OR julianday(updated_at) > julianday(?))"];
9283
9545
  const params = [since];
9284
9546
  if (filters?.project_id) {
9285
9547
  conditions.push("project_id = ?");
@@ -14696,7 +14958,7 @@ function matchesExtraFilters(task, filter) {
14696
14958
  }
14697
14959
  if (filter.tags?.length) {
14698
14960
  const taskTags = new Set(task.tags ?? []);
14699
- if (!filter.tags.every((tag) => taskTags.has(tag)))
14961
+ if (!filter.tags.some((tag) => taskTags.has(tag)))
14700
14962
  return false;
14701
14963
  }
14702
14964
  if (filter.include_subtasks !== true && filter.parent_id === undefined && task.parent_id)
@@ -14860,6 +15122,59 @@ function createLocalSqliteTodosStorageAdapter(options = {}) {
14860
15122
  init_types();
14861
15123
  import { randomUUID as randomUUID3 } from "crypto";
14862
15124
  init_creator_identity();
15125
+
15126
+ // src/lib/instant-compare.ts
15127
+ var SQLITE_STAMP = /^(\d{4})-(\d{2})-(\d{2})(?:[ T](\d{2}):(\d{2})(?::(\d{2})(?:\.(\d+))?)?(?:Z|([+-])(\d{2}):(\d{2}))?)?$/;
15128
+ var DAYS_IN_MONTH = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
15129
+ function isLeapYear(year) {
15130
+ return year % 4 === 0 && year % 100 !== 0 || year % 400 === 0;
15131
+ }
15132
+ function sqliteJulianDay(value) {
15133
+ const m = SQLITE_STAMP.exec(value);
15134
+ if (!m)
15135
+ return null;
15136
+ const year = Number(m[1]);
15137
+ const month = Number(m[2]);
15138
+ const day = Number(m[3]);
15139
+ const hour = m[4] === undefined ? 0 : Number(m[4]);
15140
+ const minute = m[5] === undefined ? 0 : Number(m[5]);
15141
+ const second = m[6] === undefined ? 0 : Number(m[6]);
15142
+ const frac = m[7];
15143
+ const sign = m[8];
15144
+ const offsetHour = m[9];
15145
+ const offsetMinute = m[10];
15146
+ if (month < 1 || month > 12)
15147
+ return null;
15148
+ if (hour > 23 || minute > 59 || second > 59)
15149
+ return null;
15150
+ const maxDay = (DAYS_IN_MONTH[month - 1] ?? 0) + (month === 2 && isLeapYear(year) ? 1 : 0);
15151
+ if (day < 1 || day > maxDay)
15152
+ return null;
15153
+ let offsetMinutes = 0;
15154
+ if (sign !== undefined) {
15155
+ const oh = Number(offsetHour ?? "0");
15156
+ const om = Number(offsetMinute ?? "0");
15157
+ if (oh > 23 || om > 59)
15158
+ return null;
15159
+ offsetMinutes = oh * 60 + om;
15160
+ if (sign === "-")
15161
+ offsetMinutes = -offsetMinutes;
15162
+ }
15163
+ const micros = frac === undefined ? 0 : Number((frac + "000000").slice(0, 6));
15164
+ const ms = Date.UTC(year, month - 1, day, hour, minute, second);
15165
+ return (ms + micros / 1000) / 86400000 + 2440587.5 - offsetMinutes / 1440;
15166
+ }
15167
+ function changedSinceStampNewer(stamp, since) {
15168
+ const stampJd = sqliteJulianDay(stamp);
15169
+ if (stampJd === null)
15170
+ return true;
15171
+ const sinceJd = sqliteJulianDay(since);
15172
+ if (sinceJd === null)
15173
+ return false;
15174
+ return stampJd > sinceJd;
15175
+ }
15176
+
15177
+ // src/storage/postgres-adapter.ts
14863
15178
  init_stale_lock_handoff();
14864
15179
 
14865
15180
  // src/storage/postgres-sync.ts
@@ -15474,11 +15789,18 @@ class PostgresJsonRecordStore {
15474
15789
  return context?.requestId ?? this.sourceMachineId ?? null;
15475
15790
  }
15476
15791
  async ensureSchema() {
15477
- this.schemaReady ??= (async () => {
15478
- for (const sql of postgresTodosSyncSchemaSql(this.tableName, this.cursorTableName)) {
15479
- await this.options.client.query(sql);
15480
- }
15481
- })();
15792
+ if (!this.schemaReady) {
15793
+ this.schemaReady = (async () => {
15794
+ await retryOnTransientPostgresError(async () => {
15795
+ for (const sql of postgresTodosSyncSchemaSql(this.tableName, this.cursorTableName)) {
15796
+ await this.options.client.query(sql);
15797
+ }
15798
+ });
15799
+ })().catch((error) => {
15800
+ this.schemaReady = null;
15801
+ throw error;
15802
+ });
15803
+ }
15482
15804
  await this.schemaReady;
15483
15805
  }
15484
15806
  async get(type, id) {
@@ -15887,13 +16209,14 @@ class PostgresJsonRecordStore {
15887
16209
  throw new Error(divergentAuditHistoryReplayError(value.id));
15888
16210
  }
15889
16211
  async withTaskParentIntegrityTransaction(fn) {
15890
- if (typeof this.options.client.transaction !== "function") {
16212
+ const transaction = this.options.client.transaction;
16213
+ if (typeof transaction !== "function") {
15891
16214
  throw new Error("TASK_PARENT_ATOMICITY_UNAVAILABLE: PostgreSQL parent writes and task deletion require transaction(callback)");
15892
16215
  }
15893
- return this.options.client.transaction(async (client) => {
16216
+ return retryOnTransientPostgresError(() => transaction(async (client) => {
15894
16217
  await client.query("/* todos:task-parent-integrity-lock */ SELECT pg_advisory_xact_lock(hashtextextended($1 || ':task-parent-integrity', 0))", [this.service]);
15895
16218
  return fn(client);
15896
- });
16219
+ }));
15897
16220
  }
15898
16221
  async upsertTaskWithPlanMembershipGuard(value, guardedPlanIds, explicitProject, context = {}, parentGuard, queryClient) {
15899
16222
  const planIds = [...new Set(guardedPlanIds.filter(Boolean))].sort();
@@ -17243,7 +17566,7 @@ async function getActiveWork2(filters, store) {
17243
17566
  }));
17244
17567
  }
17245
17568
  async function getChangedSince(since, filters, store) {
17246
- return (await listTasks2(filters ?? {}, store)).filter((task) => task.updated_at > since);
17569
+ return (await listTasks2(filters ?? {}, store)).filter((task) => changedSinceStampNewer(task.updated_at ?? "", since));
17247
17570
  }
17248
17571
  async function createProject2(input, store, context) {
17249
17572
  const timestamp2 = new Date().toISOString();
@@ -17779,6 +18102,41 @@ function compareClock(left, right) {
17779
18102
  function numberValue2(value) {
17780
18103
  return typeof value === "number" && Number.isSafeInteger(value) ? value : null;
17781
18104
  }
18105
+ var TRANSIENT_POSTGRES_SQLSTATES = new Set(["55P03", "40P01", "40001"]);
18106
+ var TRANSIENT_POSTGRES_MESSAGE_MARKERS = [
18107
+ "canceling statement due to lock timeout",
18108
+ "deadlock detected",
18109
+ "could not serialize access"
18110
+ ];
18111
+ function isTransientPostgresError(error) {
18112
+ if (typeof error !== "object" || error === null)
18113
+ return false;
18114
+ const candidate = error;
18115
+ const states = [candidate.code, candidate.errno, candidate.sqlState, candidate.sqlstate];
18116
+ if (typeof candidate.cause === "object" && candidate.cause !== null) {
18117
+ const cause = candidate.cause;
18118
+ states.push(cause.code, cause.errno, cause.sqlState, cause.sqlstate);
18119
+ }
18120
+ if (states.some((state) => typeof state === "string" && TRANSIENT_POSTGRES_SQLSTATES.has(state))) {
18121
+ return true;
18122
+ }
18123
+ const message = typeof candidate.message === "string" ? candidate.message : candidate.cause?.message;
18124
+ return typeof message === "string" && TRANSIENT_POSTGRES_MESSAGE_MARKERS.some((marker) => message.includes(marker));
18125
+ }
18126
+ async function retryOnTransientPostgresError(fn, attempts = 2, delayMs = 150) {
18127
+ let lastError;
18128
+ for (let attempt = 1;attempt <= attempts; attempt += 1) {
18129
+ try {
18130
+ return await fn();
18131
+ } catch (error) {
18132
+ lastError = error;
18133
+ if (!isTransientPostgresError(error) || attempt === attempts)
18134
+ throw error;
18135
+ await new Promise((resolve8) => setTimeout(resolve8, delayMs * attempt));
18136
+ }
18137
+ }
18138
+ throw lastError;
18139
+ }
17782
18140
  function isPostgresUniqueViolation(error) {
17783
18141
  if (typeof error !== "object" || error === null)
17784
18142
  return false;