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