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