@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/dashboard/dist/assets/index-BNQ4gJua.js +342 -0
- package/dashboard/dist/assets/index-DjzvHUWt.css +1 -0
- package/dashboard/dist/index.html +2 -2
- package/dist/cli/components/Dashboard.d.ts +1 -1
- package/dist/cli/components/Dashboard.d.ts.map +1 -1
- package/dist/cli/components/Header.d.ts +1 -1
- package/dist/cli/components/Header.d.ts.map +1 -1
- package/dist/cli/components/ProjectList.d.ts +1 -1
- package/dist/cli/components/ProjectList.d.ts.map +1 -1
- package/dist/cli/components/SearchView.d.ts +1 -1
- package/dist/cli/components/SearchView.d.ts.map +1 -1
- package/dist/cli/components/TaskDetail.d.ts +1 -1
- package/dist/cli/components/TaskDetail.d.ts.map +1 -1
- package/dist/cli/components/TaskForm.d.ts +1 -1
- package/dist/cli/components/TaskForm.d.ts.map +1 -1
- package/dist/cli/components/TaskList.d.ts +1 -1
- package/dist/cli/components/TaskList.d.ts.map +1 -1
- package/dist/cli/index.js +386 -85
- package/dist/contracts.js +341 -78
- package/dist/db/webhooks.d.ts.map +1 -1
- package/dist/index.js +385 -84
- package/dist/mcp/index.js +386 -85
- package/dist/mcp.js +4 -3
- package/dist/project-registration.js +385 -84
- package/dist/registry.js +341 -78
- package/dist/release-provenance.json +5 -5
- package/dist/server/index.js +1041 -271
- package/dist/storage.js +381 -81
- package/package.json +4 -3
- package/dashboard/dist/assets/index-DJm6m6Yy.css +0 -1
- package/dashboard/dist/assets/index-DVotjwab.js +0 -346
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.
|
|
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
|
|
11252
|
+
async appendEventOnce(event, options = {}) {
|
|
11250
11253
|
await this.init();
|
|
11251
|
-
|
|
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
|
|
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 =
|
|
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
|
-
|
|
11330
|
-
|
|
11459
|
+
...channel.webhook.headers,
|
|
11460
|
+
"X-Hasna-Timestamp": timestamp2
|
|
11331
11461
|
};
|
|
11332
|
-
|
|
11333
|
-
|
|
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
|
-
|
|
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 = {
|
|
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.
|
|
11503
|
-
|
|
11504
|
-
|
|
11505
|
-
|
|
11506
|
-
|
|
11507
|
-
|
|
11508
|
-
|
|
11509
|
-
const deliveries = options.deliver === false ? [] : await this.deliver(event);
|
|
11510
|
-
return { event, deliveries, deduped: false };
|
|
11511
|
-
}
|
|
11512
|
-
async listEvents() {
|
|
11513
|
-
|
|
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
|
|
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
|
|
11624
|
-
|
|
11625
|
-
|
|
11626
|
-
|
|
11627
|
-
|
|
11628
|
-
|
|
11629
|
-
|
|
11630
|
-
|
|
11631
|
-
|
|
11632
|
-
|
|
11633
|
-
|
|
11634
|
-
|
|
11635
|
-
|
|
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.
|
|
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: "
|
|
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
|
-
|
|
47494
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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: "
|
|
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",
|