hippo-memory 0.38.0 → 0.40.0
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/README.md +9 -0
- package/dist/api.d.ts +29 -6
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +168 -42
- package/dist/api.js.map +1 -1
- package/dist/audit.d.ts +1 -1
- package/dist/audit.d.ts.map +1 -1
- package/dist/audit.js.map +1 -1
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +20 -4
- package/dist/auth.js.map +1 -1
- package/dist/cli.js +101 -6
- package/dist/cli.js.map +1 -1
- package/dist/connectors/slack/deletion.d.ts +10 -0
- package/dist/connectors/slack/deletion.d.ts.map +1 -1
- package/dist/connectors/slack/deletion.js +21 -10
- package/dist/connectors/slack/deletion.js.map +1 -1
- package/dist/connectors/slack/dlq.d.ts +59 -1
- package/dist/connectors/slack/dlq.d.ts.map +1 -1
- package/dist/connectors/slack/dlq.js +204 -6
- package/dist/connectors/slack/dlq.js.map +1 -1
- package/dist/connectors/slack/idempotency.d.ts +12 -0
- package/dist/connectors/slack/idempotency.d.ts.map +1 -1
- package/dist/connectors/slack/idempotency.js +16 -0
- package/dist/connectors/slack/idempotency.js.map +1 -1
- package/dist/connectors/slack/ingest.d.ts +7 -1
- package/dist/connectors/slack/ingest.d.ts.map +1 -1
- package/dist/connectors/slack/ingest.js +40 -6
- package/dist/connectors/slack/ingest.js.map +1 -1
- package/dist/connectors/slack/signature.d.ts +7 -0
- package/dist/connectors/slack/signature.d.ts.map +1 -1
- package/dist/connectors/slack/signature.js +16 -9
- package/dist/connectors/slack/signature.js.map +1 -1
- package/dist/connectors/slack/tenant-routing.d.ts +15 -7
- package/dist/connectors/slack/tenant-routing.d.ts.map +1 -1
- package/dist/connectors/slack/tenant-routing.js +28 -8
- package/dist/connectors/slack/tenant-routing.js.map +1 -1
- package/dist/correction-latency.d.ts +36 -0
- package/dist/correction-latency.d.ts.map +1 -0
- package/dist/correction-latency.js +74 -0
- package/dist/correction-latency.js.map +1 -0
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +88 -1
- package/dist/db.js.map +1 -1
- package/dist/mcp/server.d.ts +7 -0
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +73 -30
- package/dist/mcp/server.js.map +1 -1
- package/dist/provenance-coverage.d.ts +18 -0
- package/dist/provenance-coverage.d.ts.map +1 -0
- package/dist/provenance-coverage.js +23 -0
- package/dist/provenance-coverage.js.map +1 -0
- package/dist/raw-archive-mirror-cleanup.d.ts +20 -0
- package/dist/raw-archive-mirror-cleanup.d.ts.map +1 -0
- package/dist/raw-archive-mirror-cleanup.js +53 -0
- package/dist/raw-archive-mirror-cleanup.js.map +1 -0
- package/dist/raw-archive.d.ts +8 -0
- package/dist/raw-archive.d.ts.map +1 -1
- package/dist/raw-archive.js +21 -7
- package/dist/raw-archive.js.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +142 -18
- package/dist/server.js.map +1 -1
- package/dist/shared.d.ts +1 -0
- package/dist/shared.d.ts.map +1 -1
- package/dist/shared.js +6 -1
- package/dist/shared.js.map +1 -1
- package/dist/src/api.js +168 -42
- package/dist/src/api.js.map +1 -1
- package/dist/src/audit.js.map +1 -1
- package/dist/src/auth.js +20 -4
- package/dist/src/auth.js.map +1 -1
- package/dist/src/cli.js +101 -6
- package/dist/src/cli.js.map +1 -1
- package/dist/src/connectors/slack/deletion.js +21 -10
- package/dist/src/connectors/slack/deletion.js.map +1 -1
- package/dist/src/connectors/slack/dlq.js +204 -6
- package/dist/src/connectors/slack/dlq.js.map +1 -1
- package/dist/src/connectors/slack/idempotency.js +16 -0
- package/dist/src/connectors/slack/idempotency.js.map +1 -1
- package/dist/src/connectors/slack/ingest.js +40 -6
- package/dist/src/connectors/slack/ingest.js.map +1 -1
- package/dist/src/connectors/slack/signature.js +16 -9
- package/dist/src/connectors/slack/signature.js.map +1 -1
- package/dist/src/connectors/slack/tenant-routing.js +28 -8
- package/dist/src/connectors/slack/tenant-routing.js.map +1 -1
- package/dist/src/correction-latency.js +74 -0
- package/dist/src/correction-latency.js.map +1 -0
- package/dist/src/db.js +88 -1
- package/dist/src/db.js.map +1 -1
- package/dist/src/mcp/server.js +73 -30
- package/dist/src/mcp/server.js.map +1 -1
- package/dist/src/provenance-coverage.js +23 -0
- package/dist/src/provenance-coverage.js.map +1 -0
- package/dist/src/raw-archive-mirror-cleanup.js +53 -0
- package/dist/src/raw-archive-mirror-cleanup.js.map +1 -0
- package/dist/src/raw-archive.js +21 -7
- package/dist/src/raw-archive.js.map +1 -1
- package/dist/src/server.js +142 -18
- package/dist/src/server.js.map +1 -1
- package/dist/src/shared.js +6 -1
- package/dist/src/shared.js.map +1 -1
- package/dist/src/store.js +50 -26
- package/dist/src/store.js.map +1 -1
- package/dist/store.d.ts +24 -0
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +50 -26
- package/dist/store.js.map +1 -1
- package/extensions/openclaw-plugin/openclaw.plugin.json +1 -1
- package/extensions/openclaw-plugin/package.json +1 -1
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
|
@@ -10,5 +10,15 @@ export interface DeletionResult {
|
|
|
10
10
|
status: DeletionStatus;
|
|
11
11
|
memoryId: string | null;
|
|
12
12
|
}
|
|
13
|
+
/**
|
|
14
|
+
* Handle Slack `message_deleted`. v0.39 commit 3 closes the prior race where
|
|
15
|
+
* the archive committed but `markEventSeen` ran on a second db handle — a
|
|
16
|
+
* crash between them left the deletion event un-acked, and the next retry
|
|
17
|
+
* hit a now-archived row and returned `not_found` instead of `duplicate`.
|
|
18
|
+
*
|
|
19
|
+
* Fix: pass `afterArchive` to `archiveRaw`, which runs inside the same
|
|
20
|
+
* SAVEPOINT as the archive itself. The slack_event_log row commits with the
|
|
21
|
+
* archive or not at all.
|
|
22
|
+
*/
|
|
13
23
|
export declare function handleMessageDeleted(ctx: Context, input: DeletionInput): DeletionResult;
|
|
14
24
|
//# sourceMappingURL=deletion.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"deletion.d.ts","sourceRoot":"","sources":["../../../src/connectors/slack/deletion.ts"],"names":[],"mappings":"AAAA,OAAO,EAAc,KAAK,OAAO,EAAE,MAAM,cAAc,CAAC;AAIxD,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,MAAM,cAAc,GAAG,UAAU,GAAG,WAAW,GAAG,WAAW,CAAC;AAEpE,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,cAAc,CAAC;IACvB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;CACzB;AAED,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,OAAO,EAAE,KAAK,EAAE,aAAa,GAAG,cAAc,
|
|
1
|
+
{"version":3,"file":"deletion.d.ts","sourceRoot":"","sources":["../../../src/connectors/slack/deletion.ts"],"names":[],"mappings":"AAAA,OAAO,EAAc,KAAK,OAAO,EAAE,MAAM,cAAc,CAAC;AAIxD,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,MAAM,cAAc,GAAG,UAAU,GAAG,WAAW,GAAG,WAAW,CAAC;AAEpE,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,cAAc,CAAC;IACvB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;CACzB;AAED;;;;;;;;;GASG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,OAAO,EAAE,KAAK,EAAE,aAAa,GAAG,cAAc,CA0CvF"}
|
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
import { archiveRaw } from '../../api.js';
|
|
2
2
|
import { openHippoDb, closeHippoDb } from '../../db.js';
|
|
3
3
|
import { markEventSeen, hasSeenEvent } from './idempotency.js';
|
|
4
|
+
/**
|
|
5
|
+
* Handle Slack `message_deleted`. v0.39 commit 3 closes the prior race where
|
|
6
|
+
* the archive committed but `markEventSeen` ran on a second db handle — a
|
|
7
|
+
* crash between them left the deletion event un-acked, and the next retry
|
|
8
|
+
* hit a now-archived row and returned `not_found` instead of `duplicate`.
|
|
9
|
+
*
|
|
10
|
+
* Fix: pass `afterArchive` to `archiveRaw`, which runs inside the same
|
|
11
|
+
* SAVEPOINT as the archive itself. The slack_event_log row commits with the
|
|
12
|
+
* archive or not at all.
|
|
13
|
+
*/
|
|
4
14
|
export function handleMessageDeleted(ctx, input) {
|
|
5
15
|
const db = openHippoDb(ctx.hippoRoot);
|
|
6
16
|
let memoryId = null;
|
|
@@ -22,6 +32,9 @@ export function handleMessageDeleted(ctx, input) {
|
|
|
22
32
|
closeHippoDb(db);
|
|
23
33
|
}
|
|
24
34
|
if (!memoryId) {
|
|
35
|
+
// No row to archive — still mark the deletion event seen so a retry returns
|
|
36
|
+
// 'duplicate'. There is nothing to roll back here, so the second-handle
|
|
37
|
+
// pattern is fine for this branch.
|
|
25
38
|
const db2 = openHippoDb(ctx.hippoRoot);
|
|
26
39
|
try {
|
|
27
40
|
markEventSeen(db2, input.eventId, null);
|
|
@@ -31,16 +44,14 @@ export function handleMessageDeleted(ctx, input) {
|
|
|
31
44
|
}
|
|
32
45
|
return { status: 'not_found', memoryId: null };
|
|
33
46
|
}
|
|
34
|
-
//
|
|
35
|
-
//
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
closeHippoDb(db3);
|
|
43
|
-
}
|
|
47
|
+
// Archive + event-log mark commit together via afterArchive. The hook
|
|
48
|
+
// receives the same db handle the archive is using, so the INSERT lives
|
|
49
|
+
// inside the SAVEPOINT.
|
|
50
|
+
archiveRaw(ctx, memoryId, `source_deleted:slack:${input.teamId}:${input.channelId}:${input.deletedTs}`, {
|
|
51
|
+
afterArchive: (sameDb, archivedId) => {
|
|
52
|
+
markEventSeen(sameDb, input.eventId, archivedId);
|
|
53
|
+
},
|
|
54
|
+
});
|
|
44
55
|
return { status: 'archived', memoryId };
|
|
45
56
|
}
|
|
46
57
|
//# sourceMappingURL=deletion.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"deletion.js","sourceRoot":"","sources":["../../../src/connectors/slack/deletion.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAgB,MAAM,cAAc,CAAC;AACxD,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AACxD,OAAO,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAgB/D,MAAM,UAAU,oBAAoB,CAAC,GAAY,EAAE,KAAoB;IACrE,MAAM,EAAE,GAAG,WAAW,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;IACtC,IAAI,QAAQ,GAAkB,IAAI,CAAC;IACnC,IAAI,CAAC;QACH,IAAI,YAAY,CAAC,EAAE,EAAE,KAAK,CAAC,OAAO,CAAC,EAAE,CAAC;YACpC,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;QACjD,CAAC;QACD,MAAM,GAAG,GAAG,WAAW,KAAK,CAAC,MAAM,IAAI,KAAK,CAAC,SAAS,IAAI,KAAK,CAAC,SAAS,EAAE,CAAC;QAC5E,yEAAyE;QACzE,kEAAkE;QAClE,0EAA0E;QAC1E,kBAAkB;QAClB,MAAM,GAAG,GAAG,EAAE;aACX,OAAO,CAAC,mFAAmF,CAAC;aAC5F,GAAG,CAAC,GAAG,EAAE,GAAG,CAAC,QAAQ,CAAgC,CAAC;QACzD,QAAQ,GAAG,GAAG,EAAE,EAAE,IAAI,IAAI,CAAC;IAC7B,CAAC;YAAS,CAAC;QACT,YAAY,CAAC,EAAE,CAAC,CAAC;IACnB,CAAC;IACD,IAAI,CAAC,QAAQ,EAAE,CAAC;QACd,MAAM,GAAG,GAAG,WAAW,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;QACvC,IAAI,CAAC;YAAC,aAAa,CAAC,GAAG,EAAE,KAAK,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;QAAC,CAAC;gBACxC,CAAC;YAAC,YAAY,CAAC,GAAG,CAAC,CAAC;QAAC,CAAC;QAC9B,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;IACjD,CAAC;IACD,
|
|
1
|
+
{"version":3,"file":"deletion.js","sourceRoot":"","sources":["../../../src/connectors/slack/deletion.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAgB,MAAM,cAAc,CAAC;AACxD,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AACxD,OAAO,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAgB/D;;;;;;;;;GASG;AACH,MAAM,UAAU,oBAAoB,CAAC,GAAY,EAAE,KAAoB;IACrE,MAAM,EAAE,GAAG,WAAW,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;IACtC,IAAI,QAAQ,GAAkB,IAAI,CAAC;IACnC,IAAI,CAAC;QACH,IAAI,YAAY,CAAC,EAAE,EAAE,KAAK,CAAC,OAAO,CAAC,EAAE,CAAC;YACpC,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;QACjD,CAAC;QACD,MAAM,GAAG,GAAG,WAAW,KAAK,CAAC,MAAM,IAAI,KAAK,CAAC,SAAS,IAAI,KAAK,CAAC,SAAS,EAAE,CAAC;QAC5E,yEAAyE;QACzE,kEAAkE;QAClE,0EAA0E;QAC1E,kBAAkB;QAClB,MAAM,GAAG,GAAG,EAAE;aACX,OAAO,CAAC,mFAAmF,CAAC;aAC5F,GAAG,CAAC,GAAG,EAAE,GAAG,CAAC,QAAQ,CAAgC,CAAC;QACzD,QAAQ,GAAG,GAAG,EAAE,EAAE,IAAI,IAAI,CAAC;IAC7B,CAAC;YAAS,CAAC;QACT,YAAY,CAAC,EAAE,CAAC,CAAC;IACnB,CAAC;IACD,IAAI,CAAC,QAAQ,EAAE,CAAC;QACd,4EAA4E;QAC5E,wEAAwE;QACxE,mCAAmC;QACnC,MAAM,GAAG,GAAG,WAAW,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;QACvC,IAAI,CAAC;YAAC,aAAa,CAAC,GAAG,EAAE,KAAK,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;QAAC,CAAC;gBACxC,CAAC;YAAC,YAAY,CAAC,GAAG,CAAC,CAAC;QAAC,CAAC;QAC9B,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;IACjD,CAAC;IACD,sEAAsE;IACtE,wEAAwE;IACxE,wBAAwB;IACxB,UAAU,CACR,GAAG,EACH,QAAQ,EACR,wBAAwB,KAAK,CAAC,MAAM,IAAI,KAAK,CAAC,SAAS,IAAI,KAAK,CAAC,SAAS,EAAE,EAC5E;QACE,YAAY,EAAE,CAAC,MAAM,EAAE,UAAU,EAAE,EAAE;YACnC,aAAa,CAAC,MAAM,EAAE,KAAK,CAAC,OAAO,EAAE,UAAU,CAAC,CAAC;QACnD,CAAC;KACF,CACF,CAAC;IACF,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,QAAQ,EAAE,CAAC;AAC1C,CAAC"}
|
|
@@ -1,21 +1,79 @@
|
|
|
1
1
|
import type { DatabaseSyncLike } from '../../db.js';
|
|
2
|
+
import type { Context } from '../../api.js';
|
|
3
|
+
export type DlqBucket = 'parse_error' | 'unroutable' | 'signature_fail';
|
|
2
4
|
export interface DlqItem {
|
|
3
5
|
id: number;
|
|
4
6
|
tenantId: string;
|
|
7
|
+
teamId: string | null;
|
|
5
8
|
rawPayload: string;
|
|
6
9
|
error: string;
|
|
7
10
|
receivedAt: string;
|
|
8
11
|
retriedAt: string | null;
|
|
12
|
+
bucket: DlqBucket | string;
|
|
13
|
+
retryCount: number;
|
|
14
|
+
signature: string | null;
|
|
15
|
+
slackTimestamp: string | null;
|
|
9
16
|
}
|
|
17
|
+
/**
|
|
18
|
+
* v0.39 commit 3: writeToDlq is now bucket-aware. `bucket` defaults to
|
|
19
|
+
* 'parse_error' to preserve the legacy single-arg call sites while letting
|
|
20
|
+
* new callers tag rows for `hippo slack dlq replay` triage.
|
|
21
|
+
*
|
|
22
|
+
* `tenantId: null` means "no tenant resolved" (unroutable team). Stored as
|
|
23
|
+
* the sentinel '__unroutable__' so the column stays NOT NULL — a mismatch
|
|
24
|
+
* the listing CLI surfaces explicitly.
|
|
25
|
+
*/
|
|
10
26
|
export interface WriteDlqOpts {
|
|
11
|
-
tenantId: string;
|
|
27
|
+
tenantId: string | null;
|
|
12
28
|
rawPayload: string;
|
|
13
29
|
error: string;
|
|
30
|
+
bucket?: DlqBucket;
|
|
31
|
+
teamId?: string | null;
|
|
32
|
+
signature?: string | null;
|
|
33
|
+
slackTimestamp?: string | null;
|
|
14
34
|
}
|
|
15
35
|
export declare function writeToDlq(db: DatabaseSyncLike, opts: WriteDlqOpts): number;
|
|
16
36
|
export declare function listDlq(db: DatabaseSyncLike, opts: {
|
|
17
37
|
tenantId: string;
|
|
18
38
|
limit?: number;
|
|
19
39
|
}): DlqItem[];
|
|
40
|
+
export declare function getDlqEntry(db: DatabaseSyncLike, id: number): DlqItem | null;
|
|
20
41
|
export declare function markDlqRetried(db: DatabaseSyncLike, id: number): void;
|
|
42
|
+
export interface ReplayDlqOpts {
|
|
43
|
+
/** Skip signature verification when the row's signature/timestamp are missing or stale. */
|
|
44
|
+
force?: boolean;
|
|
45
|
+
/** Current signing secret. If omitted, signature check is skipped (force-only path). */
|
|
46
|
+
signingSecret?: string;
|
|
47
|
+
/** Now (unix seconds) override for tests. */
|
|
48
|
+
now?: number;
|
|
49
|
+
/** Skew window override for tests. */
|
|
50
|
+
skewSeconds?: number;
|
|
51
|
+
}
|
|
52
|
+
export interface ReplayDlqResult {
|
|
53
|
+
ok: boolean;
|
|
54
|
+
status: string;
|
|
55
|
+
memoryId: string | null;
|
|
56
|
+
retryCount: number;
|
|
57
|
+
reason?: string;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Replay a DLQ row through the normal ingest path. Used by `hippo slack dlq
|
|
61
|
+
* replay <id> [--force]`.
|
|
62
|
+
*
|
|
63
|
+
* Behavior:
|
|
64
|
+
* 1. SELECT the row.
|
|
65
|
+
* 2. If signature + slack_timestamp are present and `signingSecret` is set,
|
|
66
|
+
* re-verify with the CURRENT secret (not previous). Failure → bail unless
|
|
67
|
+
* --force. Legacy rows from before v19 may have NULL signature; those
|
|
68
|
+
* require --force to replay safely.
|
|
69
|
+
* 3. Re-parse the raw_payload and dispatch to ingestMessage / handleMessageDeleted.
|
|
70
|
+
* 4. On success: mark retried_at, increment retry_count.
|
|
71
|
+
* 5. On failure: increment retry_count only, leave the row.
|
|
72
|
+
*
|
|
73
|
+
* The replay always uses the routing the deployment has NOW (current
|
|
74
|
+
* slack_workspaces table + env), not whatever was in effect when the original
|
|
75
|
+
* envelope was DLQed. That is intentional: the DLQ exists to be drained after
|
|
76
|
+
* the operator fixed the routing.
|
|
77
|
+
*/
|
|
78
|
+
export declare function replayDlqEntry(ctx: Pick<Context, 'hippoRoot'>, id: number, opts?: ReplayDlqOpts): ReplayDlqResult;
|
|
21
79
|
//# sourceMappingURL=dlq.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dlq.d.ts","sourceRoot":"","sources":["../../../src/connectors/slack/dlq.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"dlq.d.ts","sourceRoot":"","sources":["../../../src/connectors/slack/dlq.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AACpD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAQ5C,MAAM,MAAM,SAAS,GAAG,aAAa,GAAG,YAAY,GAAG,gBAAgB,CAAC;AAExE,MAAM,WAAW,OAAO;IACtB,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,MAAM,EAAE,SAAS,GAAG,MAAM,CAAC;IAC3B,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;CAC/B;AAED;;;;;;;;GAQG;AACH,MAAM,WAAW,YAAY;IAC3B,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,SAAS,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAChC;AAED,wBAAgB,UAAU,CAAC,EAAE,EAAE,gBAAgB,EAAE,IAAI,EAAE,YAAY,GAAG,MAAM,CAkB3E;AAED,wBAAgB,OAAO,CAAC,EAAE,EAAE,gBAAgB,EAAE,IAAI,EAAE;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,OAAO,EAAE,CAYnG;AAED,wBAAgB,WAAW,CAAC,EAAE,EAAE,gBAAgB,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,GAAG,IAAI,CAW5E;AAkBD,wBAAgB,cAAc,CAAC,EAAE,EAAE,gBAAgB,EAAE,EAAE,EAAE,MAAM,GAAG,IAAI,CAGrE;AAED,MAAM,WAAW,aAAa;IAC5B,2FAA2F;IAC3F,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,wFAAwF;IACxF,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,6CAA6C;IAC7C,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,sCAAsC;IACtC,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,OAAO,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,cAAc,CAC5B,GAAG,EAAE,IAAI,CAAC,OAAO,EAAE,WAAW,CAAC,EAC/B,EAAE,EAAE,MAAM,EACV,IAAI,GAAE,aAAkB,GACvB,eAAe,CAmIjB"}
|
|
@@ -1,23 +1,221 @@
|
|
|
1
|
+
import { openHippoDb, closeHippoDb } from '../../db.js';
|
|
2
|
+
import { ingestMessage } from './ingest.js';
|
|
3
|
+
import { resolveTenantForTeam } from './tenant-routing.js';
|
|
4
|
+
import { verifySlackSignature } from './signature.js';
|
|
5
|
+
import { isSlackEventEnvelope, isSlackMessageEvent } from './types.js';
|
|
6
|
+
import { handleMessageDeleted } from './deletion.js';
|
|
1
7
|
export function writeToDlq(db, opts) {
|
|
2
8
|
const result = db
|
|
3
|
-
.prepare(`INSERT INTO slack_dlq
|
|
4
|
-
|
|
9
|
+
.prepare(`INSERT INTO slack_dlq
|
|
10
|
+
(tenant_id, team_id, raw_payload, error, received_at, bucket, signature, slack_timestamp)
|
|
11
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
12
|
+
.run(opts.tenantId ?? '__unroutable__', opts.teamId ?? null, opts.rawPayload, opts.error, new Date().toISOString(), opts.bucket ?? 'parse_error', opts.signature ?? null, opts.slackTimestamp ?? null);
|
|
5
13
|
return Number(result.lastInsertRowid);
|
|
6
14
|
}
|
|
7
15
|
export function listDlq(db, opts) {
|
|
8
16
|
const rows = db
|
|
9
|
-
.prepare(`SELECT id, tenant_id, raw_payload, error, received_at, retried_at
|
|
17
|
+
.prepare(`SELECT id, tenant_id, team_id, raw_payload, error, received_at, retried_at,
|
|
18
|
+
bucket, retry_count, signature, slack_timestamp
|
|
19
|
+
FROM slack_dlq
|
|
20
|
+
WHERE tenant_id = ?
|
|
21
|
+
ORDER BY received_at ASC
|
|
22
|
+
LIMIT ?`)
|
|
10
23
|
.all(opts.tenantId, opts.limit ?? 100);
|
|
11
|
-
return rows.map(
|
|
24
|
+
return rows.map(rowToItem);
|
|
25
|
+
}
|
|
26
|
+
export function getDlqEntry(db, id) {
|
|
27
|
+
const row = db
|
|
28
|
+
.prepare(`SELECT id, tenant_id, team_id, raw_payload, error, received_at, retried_at,
|
|
29
|
+
bucket, retry_count, signature, slack_timestamp
|
|
30
|
+
FROM slack_dlq
|
|
31
|
+
WHERE id = ?`)
|
|
32
|
+
.get(id);
|
|
33
|
+
if (!row)
|
|
34
|
+
return null;
|
|
35
|
+
return rowToItem(row);
|
|
36
|
+
}
|
|
37
|
+
function rowToItem(r) {
|
|
38
|
+
return {
|
|
12
39
|
id: Number(r.id),
|
|
13
40
|
tenantId: String(r.tenant_id),
|
|
41
|
+
teamId: r.team_id == null ? null : String(r.team_id),
|
|
14
42
|
rawPayload: String(r.raw_payload),
|
|
15
43
|
error: String(r.error),
|
|
16
44
|
receivedAt: String(r.received_at),
|
|
17
45
|
retriedAt: r.retried_at == null ? null : String(r.retried_at),
|
|
18
|
-
|
|
46
|
+
bucket: r.bucket == null ? 'parse_error' : String(r.bucket),
|
|
47
|
+
retryCount: Number(r.retry_count ?? 0),
|
|
48
|
+
signature: r.signature == null ? null : String(r.signature),
|
|
49
|
+
slackTimestamp: r.slack_timestamp == null ? null : String(r.slack_timestamp),
|
|
50
|
+
};
|
|
19
51
|
}
|
|
20
52
|
export function markDlqRetried(db, id) {
|
|
21
|
-
db.prepare(`UPDATE slack_dlq SET retried_at =
|
|
53
|
+
db.prepare(`UPDATE slack_dlq SET retried_at = ?, retry_count = retry_count + 1 WHERE id = ?`)
|
|
54
|
+
.run(new Date().toISOString(), id);
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Replay a DLQ row through the normal ingest path. Used by `hippo slack dlq
|
|
58
|
+
* replay <id> [--force]`.
|
|
59
|
+
*
|
|
60
|
+
* Behavior:
|
|
61
|
+
* 1. SELECT the row.
|
|
62
|
+
* 2. If signature + slack_timestamp are present and `signingSecret` is set,
|
|
63
|
+
* re-verify with the CURRENT secret (not previous). Failure → bail unless
|
|
64
|
+
* --force. Legacy rows from before v19 may have NULL signature; those
|
|
65
|
+
* require --force to replay safely.
|
|
66
|
+
* 3. Re-parse the raw_payload and dispatch to ingestMessage / handleMessageDeleted.
|
|
67
|
+
* 4. On success: mark retried_at, increment retry_count.
|
|
68
|
+
* 5. On failure: increment retry_count only, leave the row.
|
|
69
|
+
*
|
|
70
|
+
* The replay always uses the routing the deployment has NOW (current
|
|
71
|
+
* slack_workspaces table + env), not whatever was in effect when the original
|
|
72
|
+
* envelope was DLQed. That is intentional: the DLQ exists to be drained after
|
|
73
|
+
* the operator fixed the routing.
|
|
74
|
+
*/
|
|
75
|
+
export function replayDlqEntry(ctx, id, opts = {}) {
|
|
76
|
+
const db = openHippoDb(ctx.hippoRoot);
|
|
77
|
+
let row;
|
|
78
|
+
try {
|
|
79
|
+
row = getDlqEntry(db, id);
|
|
80
|
+
}
|
|
81
|
+
finally {
|
|
82
|
+
closeHippoDb(db);
|
|
83
|
+
}
|
|
84
|
+
if (!row) {
|
|
85
|
+
return { ok: false, status: 'not_found', memoryId: null, retryCount: 0, reason: `dlq id ${id} not found` };
|
|
86
|
+
}
|
|
87
|
+
// Signature verification (current secret, not previous).
|
|
88
|
+
if (!opts.force) {
|
|
89
|
+
if (!row.signature || !row.slackTimestamp) {
|
|
90
|
+
return {
|
|
91
|
+
ok: false,
|
|
92
|
+
status: 'sig_missing',
|
|
93
|
+
memoryId: null,
|
|
94
|
+
retryCount: row.retryCount,
|
|
95
|
+
reason: 'legacy row without signature/timestamp; pass --force to replay',
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
if (opts.signingSecret) {
|
|
99
|
+
const ok = verifySlackSignature({
|
|
100
|
+
rawBody: row.rawPayload,
|
|
101
|
+
signature: row.signature,
|
|
102
|
+
timestamp: row.slackTimestamp,
|
|
103
|
+
signingSecret: opts.signingSecret,
|
|
104
|
+
now: opts.now,
|
|
105
|
+
// Replays happen long after the fact — give them a wider skew unless overridden.
|
|
106
|
+
skewSeconds: opts.skewSeconds ?? 60 * 60 * 24 * 365,
|
|
107
|
+
});
|
|
108
|
+
if (!ok) {
|
|
109
|
+
return {
|
|
110
|
+
ok: false,
|
|
111
|
+
status: 'sig_fail',
|
|
112
|
+
memoryId: null,
|
|
113
|
+
retryCount: row.retryCount,
|
|
114
|
+
reason: 'signature did not verify against current SLACK_SIGNING_SECRET; pass --force to replay anyway',
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
// Parse + dispatch.
|
|
120
|
+
let parsed;
|
|
121
|
+
try {
|
|
122
|
+
parsed = JSON.parse(row.rawPayload);
|
|
123
|
+
}
|
|
124
|
+
catch (e) {
|
|
125
|
+
bumpRetryCount(ctx.hippoRoot, id);
|
|
126
|
+
return {
|
|
127
|
+
ok: false,
|
|
128
|
+
status: 'parse_error',
|
|
129
|
+
memoryId: null,
|
|
130
|
+
retryCount: row.retryCount + 1,
|
|
131
|
+
reason: `still unparseable: ${e.message}`,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
if (!isSlackEventEnvelope(parsed)) {
|
|
135
|
+
bumpRetryCount(ctx.hippoRoot, id);
|
|
136
|
+
return {
|
|
137
|
+
ok: false,
|
|
138
|
+
status: 'unhandled',
|
|
139
|
+
memoryId: null,
|
|
140
|
+
retryCount: row.retryCount + 1,
|
|
141
|
+
reason: 'not an event_callback envelope',
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
// Resolve tenant against current state. If still unroutable, bail.
|
|
145
|
+
const db2 = openHippoDb(ctx.hippoRoot);
|
|
146
|
+
let tenant;
|
|
147
|
+
try {
|
|
148
|
+
tenant = resolveTenantForTeam(db2, parsed.team_id);
|
|
149
|
+
}
|
|
150
|
+
finally {
|
|
151
|
+
closeHippoDb(db2);
|
|
152
|
+
}
|
|
153
|
+
if (!tenant) {
|
|
154
|
+
bumpRetryCount(ctx.hippoRoot, id);
|
|
155
|
+
return {
|
|
156
|
+
ok: false,
|
|
157
|
+
status: 'unroutable',
|
|
158
|
+
memoryId: null,
|
|
159
|
+
retryCount: row.retryCount + 1,
|
|
160
|
+
reason: `team_id ${parsed.team_id} still unroutable`,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
const replayCtx = {
|
|
164
|
+
hippoRoot: ctx.hippoRoot,
|
|
165
|
+
tenantId: tenant,
|
|
166
|
+
actor: 'connector:slack:replay',
|
|
167
|
+
};
|
|
168
|
+
const inner = parsed.event;
|
|
169
|
+
if (!isSlackMessageEvent(inner)) {
|
|
170
|
+
bumpRetryCount(ctx.hippoRoot, id);
|
|
171
|
+
return {
|
|
172
|
+
ok: false,
|
|
173
|
+
status: 'unhandled',
|
|
174
|
+
memoryId: null,
|
|
175
|
+
retryCount: row.retryCount + 1,
|
|
176
|
+
reason: `unhandled inner event type`,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
if (inner.subtype === 'message_deleted' && inner.deleted_ts) {
|
|
180
|
+
const r = handleMessageDeleted(replayCtx, {
|
|
181
|
+
teamId: parsed.team_id,
|
|
182
|
+
channelId: inner.channel,
|
|
183
|
+
deletedTs: inner.deleted_ts,
|
|
184
|
+
eventId: parsed.event_id,
|
|
185
|
+
});
|
|
186
|
+
markRetried(ctx.hippoRoot, id);
|
|
187
|
+
return { ok: true, status: r.status, memoryId: r.memoryId, retryCount: row.retryCount + 1 };
|
|
188
|
+
}
|
|
189
|
+
const result = ingestMessage(replayCtx, {
|
|
190
|
+
teamId: parsed.team_id,
|
|
191
|
+
channel: {
|
|
192
|
+
id: inner.channel,
|
|
193
|
+
is_private: inner.channel_type !== 'channel',
|
|
194
|
+
is_im: inner.channel_type === 'im',
|
|
195
|
+
is_mpim: inner.channel_type === 'mpim',
|
|
196
|
+
},
|
|
197
|
+
message: inner,
|
|
198
|
+
eventId: parsed.event_id,
|
|
199
|
+
});
|
|
200
|
+
markRetried(ctx.hippoRoot, id);
|
|
201
|
+
return { ok: true, status: result.status, memoryId: result.memoryId, retryCount: row.retryCount + 1 };
|
|
202
|
+
}
|
|
203
|
+
function markRetried(hippoRoot, id) {
|
|
204
|
+
const db = openHippoDb(hippoRoot);
|
|
205
|
+
try {
|
|
206
|
+
markDlqRetried(db, id);
|
|
207
|
+
}
|
|
208
|
+
finally {
|
|
209
|
+
closeHippoDb(db);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
function bumpRetryCount(hippoRoot, id) {
|
|
213
|
+
const db = openHippoDb(hippoRoot);
|
|
214
|
+
try {
|
|
215
|
+
db.prepare(`UPDATE slack_dlq SET retry_count = retry_count + 1 WHERE id = ?`).run(id);
|
|
216
|
+
}
|
|
217
|
+
finally {
|
|
218
|
+
closeHippoDb(db);
|
|
219
|
+
}
|
|
22
220
|
}
|
|
23
221
|
//# sourceMappingURL=dlq.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dlq.js","sourceRoot":"","sources":["../../../src/connectors/slack/dlq.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"dlq.js","sourceRoot":"","sources":["../../../src/connectors/slack/dlq.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,4 +1,16 @@
|
|
|
1
1
|
import type { DatabaseSyncLike } from '../../db.js';
|
|
2
|
+
/**
|
|
3
|
+
* Thrown by the ingest afterWrite hook when a concurrent worker has already
|
|
4
|
+
* inserted slack_event_log for the same event_id. The throw propagates out of
|
|
5
|
+
* writeEntry's SAVEPOINT, rolling back the duplicate memory write so exactly
|
|
6
|
+
* one memory row exists per Slack event_id even under two-worker races.
|
|
7
|
+
*
|
|
8
|
+
* Caller maps to `{status: 'skipped_duplicate'}` at the public API boundary.
|
|
9
|
+
*/
|
|
10
|
+
export declare class DuplicateEventError extends Error {
|
|
11
|
+
readonly eventId: string;
|
|
12
|
+
constructor(eventId: string);
|
|
13
|
+
}
|
|
2
14
|
export declare function hasSeenEvent(db: DatabaseSyncLike, eventId: string): boolean;
|
|
3
15
|
export declare function markEventSeen(db: DatabaseSyncLike, eventId: string, memoryId: string | null): void;
|
|
4
16
|
export declare function lookupMemoryByEvent(db: DatabaseSyncLike, eventId: string): string | null;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"idempotency.d.ts","sourceRoot":"","sources":["../../../src/connectors/slack/idempotency.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAEpD,wBAAgB,YAAY,CAAC,EAAE,EAAE,gBAAgB,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAG3E;AAED,wBAAgB,aAAa,CAAC,EAAE,EAAE,gBAAgB,EAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,CAGlG;AAED,wBAAgB,mBAAmB,CAAC,EAAE,EAAE,gBAAgB,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAKxF"}
|
|
1
|
+
{"version":3,"file":"idempotency.d.ts","sourceRoot":"","sources":["../../../src/connectors/slack/idempotency.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAEpD;;;;;;;GAOG;AACH,qBAAa,mBAAoB,SAAQ,KAAK;IAC5C,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;gBACb,OAAO,EAAE,MAAM;CAK5B;AAED,wBAAgB,YAAY,CAAC,EAAE,EAAE,gBAAgB,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAG3E;AAED,wBAAgB,aAAa,CAAC,EAAE,EAAE,gBAAgB,EAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,CAGlG;AAED,wBAAgB,mBAAmB,CAAC,EAAE,EAAE,gBAAgB,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAKxF"}
|
|
@@ -1,3 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Thrown by the ingest afterWrite hook when a concurrent worker has already
|
|
3
|
+
* inserted slack_event_log for the same event_id. The throw propagates out of
|
|
4
|
+
* writeEntry's SAVEPOINT, rolling back the duplicate memory write so exactly
|
|
5
|
+
* one memory row exists per Slack event_id even under two-worker races.
|
|
6
|
+
*
|
|
7
|
+
* Caller maps to `{status: 'skipped_duplicate'}` at the public API boundary.
|
|
8
|
+
*/
|
|
9
|
+
export class DuplicateEventError extends Error {
|
|
10
|
+
eventId;
|
|
11
|
+
constructor(eventId) {
|
|
12
|
+
super(`duplicate slack event_id: ${eventId}`);
|
|
13
|
+
this.name = 'DuplicateEventError';
|
|
14
|
+
this.eventId = eventId;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
1
17
|
export function hasSeenEvent(db, eventId) {
|
|
2
18
|
const row = db.prepare(`SELECT 1 FROM slack_event_log WHERE event_id = ?`).get(eventId);
|
|
3
19
|
return !!row;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"idempotency.js","sourceRoot":"","sources":["../../../src/connectors/slack/idempotency.ts"],"names":[],"mappings":"AAEA,MAAM,UAAU,YAAY,CAAC,EAAoB,EAAE,OAAe;IAChE,MAAM,GAAG,GAAG,EAAE,CAAC,OAAO,CAAC,kDAAkD,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;IACxF,OAAO,CAAC,CAAC,GAAG,CAAC;AACf,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,EAAoB,EAAE,OAAe,EAAE,QAAuB;IAC1F,EAAE,CAAC,OAAO,CAAC,2FAA2F,CAAC;SACpG,GAAG,CAAC,OAAO,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,EAAE,QAAQ,CAAC,CAAC;AACtD,CAAC;AAED,MAAM,UAAU,mBAAmB,CAAC,EAAoB,EAAE,OAAe;IACvE,MAAM,GAAG,GAAG,EAAE,CAAC,OAAO,CAAC,0DAA0D,CAAC,CAAC,GAAG,CAAC,OAAO,CAEjF,CAAC;IACd,OAAO,GAAG,EAAE,SAAS,IAAI,IAAI,CAAC;AAChC,CAAC"}
|
|
1
|
+
{"version":3,"file":"idempotency.js","sourceRoot":"","sources":["../../../src/connectors/slack/idempotency.ts"],"names":[],"mappings":"AAEA;;;;;;;GAOG;AACH,MAAM,OAAO,mBAAoB,SAAQ,KAAK;IACnC,OAAO,CAAS;IACzB,YAAY,OAAe;QACzB,KAAK,CAAC,6BAA6B,OAAO,EAAE,CAAC,CAAC;QAC9C,IAAI,CAAC,IAAI,GAAG,qBAAqB,CAAC;QAClC,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;IACzB,CAAC;CACF;AAED,MAAM,UAAU,YAAY,CAAC,EAAoB,EAAE,OAAe;IAChE,MAAM,GAAG,GAAG,EAAE,CAAC,OAAO,CAAC,kDAAkD,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;IACxF,OAAO,CAAC,CAAC,GAAG,CAAC;AACf,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,EAAoB,EAAE,OAAe,EAAE,QAAuB;IAC1F,EAAE,CAAC,OAAO,CAAC,2FAA2F,CAAC;SACpG,GAAG,CAAC,OAAO,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,EAAE,QAAQ,CAAC,CAAC;AACtD,CAAC;AAED,MAAM,UAAU,mBAAmB,CAAC,EAAoB,EAAE,OAAe;IACvE,MAAM,GAAG,GAAG,EAAE,CAAC,OAAO,CAAC,0DAA0D,CAAC,CAAC,GAAG,CAAC,OAAO,CAEjF,CAAC;IACd,OAAO,GAAG,EAAE,SAAS,IAAI,IAAI,CAAC;AAChC,CAAC"}
|
|
@@ -8,7 +8,7 @@ export interface IngestInput {
|
|
|
8
8
|
/** Slack event_id for the envelope (or for backfill, a synthesized stable id). */
|
|
9
9
|
eventId: string;
|
|
10
10
|
}
|
|
11
|
-
export type IngestStatus = 'ingested' | 'duplicate' | 'skipped';
|
|
11
|
+
export type IngestStatus = 'ingested' | 'duplicate' | 'skipped' | 'skipped_duplicate';
|
|
12
12
|
export interface IngestResult {
|
|
13
13
|
status: IngestStatus;
|
|
14
14
|
memoryId: string | null;
|
|
@@ -20,6 +20,12 @@ export interface IngestResult {
|
|
|
20
20
|
* - The memory write and the slack_event_log mark commit atomically through
|
|
21
21
|
* `api.remember`'s `afterWrite` hook — a crash between the two cannot
|
|
22
22
|
* produce a duplicate on the next retry.
|
|
23
|
+
* - The afterWrite hook uses an explicit `INSERT OR IGNORE` + changes-check:
|
|
24
|
+
* if a concurrent worker beat us to slack_event_log between the pre-check
|
|
25
|
+
* and the SAVEPOINT, we throw `DuplicateEventError` to roll back the memory
|
|
26
|
+
* write. The pre-check `hasSeenEvent` stays as a fast path for the common
|
|
27
|
+
* already-seen case, but the afterWrite throw is what makes idempotency
|
|
28
|
+
* correct under two-worker concurrency (v0.39 commit 3 fix).
|
|
23
29
|
* - Empty-body messages return 'skipped' but still mark seen so a replay
|
|
24
30
|
* returns 'duplicate' rather than re-running the transform.
|
|
25
31
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ingest.d.ts","sourceRoot":"","sources":["../../../src/connectors/slack/ingest.ts"],"names":[],"mappings":"AAAA,OAAO,EAAY,KAAK,OAAO,EAAE,MAAM,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"ingest.d.ts","sourceRoot":"","sources":["../../../src/connectors/slack/ingest.ts"],"names":[],"mappings":"AAAA,OAAO,EAAY,KAAK,OAAO,EAAE,MAAM,cAAc,CAAC;AAStD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAC9C,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAEpD,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,WAAW,CAAC;IACrB,OAAO,EAAE,iBAAiB,CAAC;IAC3B,kFAAkF;IAClF,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,MAAM,YAAY,GAAG,UAAU,GAAG,WAAW,GAAG,SAAS,GAAG,mBAAmB,CAAC;AAEtF,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,YAAY,CAAC;IACrB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;CACzB;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,OAAO,EAAE,KAAK,EAAE,WAAW,GAAG,YAAY,CA+D5E"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { remember } from '../../api.js';
|
|
2
2
|
import { openHippoDb, closeHippoDb } from '../../db.js';
|
|
3
|
-
import { hasSeenEvent, markEventSeen, lookupMemoryByEvent } from './idempotency.js';
|
|
3
|
+
import { hasSeenEvent, markEventSeen, lookupMemoryByEvent, DuplicateEventError, } from './idempotency.js';
|
|
4
4
|
import { messageToRememberOpts } from './transform.js';
|
|
5
5
|
/**
|
|
6
6
|
* Ingest a Slack message into hippo as a kind='raw' memory.
|
|
@@ -9,6 +9,12 @@ import { messageToRememberOpts } from './transform.js';
|
|
|
9
9
|
* - The memory write and the slack_event_log mark commit atomically through
|
|
10
10
|
* `api.remember`'s `afterWrite` hook — a crash between the two cannot
|
|
11
11
|
* produce a duplicate on the next retry.
|
|
12
|
+
* - The afterWrite hook uses an explicit `INSERT OR IGNORE` + changes-check:
|
|
13
|
+
* if a concurrent worker beat us to slack_event_log between the pre-check
|
|
14
|
+
* and the SAVEPOINT, we throw `DuplicateEventError` to roll back the memory
|
|
15
|
+
* write. The pre-check `hasSeenEvent` stays as a fast path for the common
|
|
16
|
+
* already-seen case, but the afterWrite throw is what makes idempotency
|
|
17
|
+
* correct under two-worker concurrency (v0.39 commit 3 fix).
|
|
12
18
|
* - Empty-body messages return 'skipped' but still mark seen so a replay
|
|
13
19
|
* returns 'duplicate' rather than re-running the transform.
|
|
14
20
|
*/
|
|
@@ -39,10 +45,38 @@ export function ingestMessage(ctx, input) {
|
|
|
39
45
|
// so the memory row and the slack_event_log row commit (or roll back)
|
|
40
46
|
// together. Slack's 1-minute retry window can no longer produce a duplicate
|
|
41
47
|
// via the crash-between-handles race.
|
|
42
|
-
|
|
43
|
-
...
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
48
|
+
try {
|
|
49
|
+
const result = remember({ ...ctx, actor: ctx.actor || 'connector:slack' }, {
|
|
50
|
+
...opts,
|
|
51
|
+
afterWrite: (innerDb, memoryId) => {
|
|
52
|
+
const inserted = innerDb
|
|
53
|
+
.prepare(`INSERT OR IGNORE INTO slack_event_log (event_id, ingested_at, memory_id) VALUES (?, ?, ?)`)
|
|
54
|
+
.run(input.eventId, new Date().toISOString(), memoryId);
|
|
55
|
+
if ((Number(inserted.changes ?? 0)) === 0) {
|
|
56
|
+
// Two-worker race: another writer reserved this event_id between
|
|
57
|
+
// the pre-check and the write. Throw to roll back the SAVEPOINT
|
|
58
|
+
// in writeEntry — the memory row gets discarded, idempotency
|
|
59
|
+
// holds, exactly one memory exists for this event_id.
|
|
60
|
+
throw new DuplicateEventError(input.eventId);
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
return { status: 'ingested', memoryId: result.id };
|
|
65
|
+
}
|
|
66
|
+
catch (e) {
|
|
67
|
+
if (e instanceof DuplicateEventError) {
|
|
68
|
+
// The other worker's memory row is already committed. Return its id
|
|
69
|
+
// so the caller behaves identically to the fast-path 'duplicate' branch.
|
|
70
|
+
const db3 = openHippoDb(ctx.hippoRoot);
|
|
71
|
+
try {
|
|
72
|
+
const cachedId = lookupMemoryByEvent(db3, input.eventId);
|
|
73
|
+
return { status: 'skipped_duplicate', memoryId: cachedId };
|
|
74
|
+
}
|
|
75
|
+
finally {
|
|
76
|
+
closeHippoDb(db3);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
throw e;
|
|
80
|
+
}
|
|
47
81
|
}
|
|
48
82
|
//# sourceMappingURL=ingest.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ingest.js","sourceRoot":"","sources":["../../../src/connectors/slack/ingest.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAgB,MAAM,cAAc,CAAC;AACtD,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AACxD,OAAO,
|
|
1
|
+
{"version":3,"file":"ingest.js","sourceRoot":"","sources":["../../../src/connectors/slack/ingest.ts"],"names":[],"mappings":"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"}
|
|
@@ -3,6 +3,13 @@ export interface VerifyOpts {
|
|
|
3
3
|
timestamp: string;
|
|
4
4
|
signature: string;
|
|
5
5
|
signingSecret: string;
|
|
6
|
+
/**
|
|
7
|
+
* Previous signing secret during a rotation. v0.39 commit 3: deploy with
|
|
8
|
+
* both `SLACK_SIGNING_SECRET` (new) and `SLACK_SIGNING_SECRET_PREVIOUS` (old)
|
|
9
|
+
* set, verify both work, drop previous after rollover. The verifier tries
|
|
10
|
+
* `signingSecret` first, then `previousSecret` if that fails.
|
|
11
|
+
*/
|
|
12
|
+
previousSecret?: string;
|
|
6
13
|
/** Current unix seconds. Injectable for tests. */
|
|
7
14
|
now?: number;
|
|
8
15
|
/** Max allowed skew in seconds. Default 5 minutes (Slack's recommendation). */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"signature.d.ts","sourceRoot":"","sources":["../../../src/connectors/slack/signature.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,aAAa,EAAE,MAAM,CAAC;IACtB,kDAAkD;IAClD,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,+EAA+E;IAC/E,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;
|
|
1
|
+
{"version":3,"file":"signature.d.ts","sourceRoot":"","sources":["../../../src/connectors/slack/signature.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,aAAa,EAAE,MAAM,CAAC;IACtB;;;;;OAKG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,kDAAkD;IAClD,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,+EAA+E;IAC/E,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAgBD,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAU9D"}
|
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
import { createHmac, timingSafeEqual } from 'crypto';
|
|
2
|
+
function verifyOne(rawBody, signature, timestamp, secret) {
|
|
3
|
+
if (!signature.startsWith('v0='))
|
|
4
|
+
return false;
|
|
5
|
+
const expected = `v0=${createHmac('sha256', secret).update(`v0:${timestamp}:${rawBody}`).digest('hex')}`;
|
|
6
|
+
const a = Buffer.from(signature, 'utf8');
|
|
7
|
+
const b = Buffer.from(expected, 'utf8');
|
|
8
|
+
if (a.length !== b.length)
|
|
9
|
+
return false;
|
|
10
|
+
return timingSafeEqual(a, b);
|
|
11
|
+
}
|
|
2
12
|
export function verifySlackSignature(opts) {
|
|
3
|
-
const { rawBody, timestamp, signature, signingSecret } = opts;
|
|
13
|
+
const { rawBody, timestamp, signature, signingSecret, previousSecret } = opts;
|
|
4
14
|
const now = opts.now ?? Math.floor(Date.now() / 1000);
|
|
5
15
|
const skew = opts.skewSeconds ?? 5 * 60;
|
|
6
16
|
const ts = Number(timestamp);
|
|
@@ -8,13 +18,10 @@ export function verifySlackSignature(opts) {
|
|
|
8
18
|
return false;
|
|
9
19
|
if (Math.abs(now - ts) > skew)
|
|
10
20
|
return false;
|
|
11
|
-
if (
|
|
12
|
-
return
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
if (a.length !== b.length)
|
|
17
|
-
return false;
|
|
18
|
-
return timingSafeEqual(a, b);
|
|
21
|
+
if (verifyOne(rawBody, signature, timestamp, signingSecret))
|
|
22
|
+
return true;
|
|
23
|
+
if (previousSecret && verifyOne(rawBody, signature, timestamp, previousSecret))
|
|
24
|
+
return true;
|
|
25
|
+
return false;
|
|
19
26
|
}
|
|
20
27
|
//# sourceMappingURL=signature.js.map
|