@agentworkforce/sage 1.1.3 → 1.2.1
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/dist/app.d.ts.map +1 -1
- package/dist/app.js +54 -29
- package/dist/e2e/e2e-harness.d.ts +36 -0
- package/dist/e2e/e2e-harness.d.ts.map +1 -0
- package/dist/e2e/e2e-harness.js +278 -0
- package/dist/e2e/mock-cloud-proxy-server.d.ts +25 -0
- package/dist/e2e/mock-cloud-proxy-server.d.ts.map +1 -0
- package/dist/e2e/mock-cloud-proxy-server.js +149 -0
- package/dist/e2e/mock-relayfile-server.d.ts +35 -0
- package/dist/e2e/mock-relayfile-server.d.ts.map +1 -0
- package/dist/e2e/mock-relayfile-server.js +488 -0
- package/dist/integrations/freshness-envelope.js +1 -1
- package/dist/integrations/github.d.ts +24 -1
- package/dist/integrations/github.d.ts.map +1 -1
- package/dist/integrations/github.js +116 -1
- package/dist/integrations/linear-ingress.d.ts +30 -0
- package/dist/integrations/linear-ingress.d.ts.map +1 -0
- package/dist/integrations/linear-ingress.js +58 -0
- package/dist/integrations/notion-ingress.d.ts +26 -0
- package/dist/integrations/notion-ingress.d.ts.map +1 -0
- package/dist/integrations/notion-ingress.js +70 -0
- package/dist/integrations/provider-ingress-dedup.d.ts +14 -0
- package/dist/integrations/provider-ingress-dedup.d.ts.map +1 -0
- package/dist/integrations/provider-ingress-dedup.js +35 -0
- package/dist/integrations/provider-ingress-dedup.test.d.ts +2 -0
- package/dist/integrations/provider-ingress-dedup.test.d.ts.map +1 -0
- package/dist/integrations/provider-ingress-dedup.test.js +55 -0
- package/dist/integrations/provider-write-facade.d.ts +80 -0
- package/dist/integrations/provider-write-facade.d.ts.map +1 -0
- package/dist/integrations/provider-write-facade.js +417 -0
- package/dist/integrations/provider-write-facade.test.d.ts +2 -0
- package/dist/integrations/provider-write-facade.test.d.ts.map +1 -0
- package/dist/integrations/provider-write-facade.test.js +247 -0
- package/dist/integrations/read-your-writes.test.d.ts +2 -0
- package/dist/integrations/read-your-writes.test.d.ts.map +1 -0
- package/dist/integrations/read-your-writes.test.js +170 -0
- package/dist/integrations/recent-actions-overlay.d.ts +1 -0
- package/dist/integrations/recent-actions-overlay.d.ts.map +1 -1
- package/dist/integrations/recent-actions-overlay.js +3 -0
- package/dist/integrations/relayfile-reader-envelope.test.d.ts +2 -0
- package/dist/integrations/relayfile-reader-envelope.test.d.ts.map +1 -0
- package/dist/integrations/relayfile-reader-envelope.test.js +198 -0
- package/dist/integrations/relayfile-reader.d.ts +20 -1
- package/dist/integrations/relayfile-reader.d.ts.map +1 -1
- package/dist/integrations/relayfile-reader.js +334 -48
- package/dist/integrations/slack-egress.d.ts +2 -1
- package/dist/integrations/slack-egress.d.ts.map +1 -1
- package/dist/integrations/slack-egress.js +28 -1
- package/dist/observability.e2e.test.d.ts +2 -0
- package/dist/observability.e2e.test.d.ts.map +1 -0
- package/dist/observability.e2e.test.js +411 -0
- package/package.json +9 -4
|
@@ -60,11 +60,39 @@ function encodeRepoPath(path) {
|
|
|
60
60
|
.map((segment) => encodeURIComponent(segment))
|
|
61
61
|
.join('/');
|
|
62
62
|
}
|
|
63
|
+
function encodePathSegment(value) {
|
|
64
|
+
return encodeURIComponent(String(value));
|
|
65
|
+
}
|
|
66
|
+
function compactRecord(input) {
|
|
67
|
+
const output = {};
|
|
68
|
+
for (const [key, value] of Object.entries(input)) {
|
|
69
|
+
if (value !== undefined) {
|
|
70
|
+
output[key] = value;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return output;
|
|
74
|
+
}
|
|
75
|
+
function createdResource(response) {
|
|
76
|
+
return {
|
|
77
|
+
id: response.id,
|
|
78
|
+
url: response.html_url ?? response.url ?? '',
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
function buildIssueCommentPath(owner, repo, number, commentId) {
|
|
82
|
+
return `/github/repos/${encodePathSegment(owner)}/${encodePathSegment(repo)}/issues/${number}/comments/${encodePathSegment(commentId)}.json`;
|
|
83
|
+
}
|
|
84
|
+
function buildPRCommentPath(owner, repo, number, commentId) {
|
|
85
|
+
return `/github/repos/${encodePathSegment(owner)}/${encodePathSegment(repo)}/pulls/${number}/comments/${encodePathSegment(commentId)}.json`;
|
|
86
|
+
}
|
|
87
|
+
function buildPRReviewPath(owner, repo, number, reviewId) {
|
|
88
|
+
return `/github/repos/${encodePathSegment(owner)}/${encodePathSegment(repo)}/pulls/${number}/reviews/${encodePathSegment(reviewId)}.json`;
|
|
89
|
+
}
|
|
63
90
|
export class GitHubIntegration {
|
|
64
91
|
nango;
|
|
65
92
|
connectionId;
|
|
66
93
|
providerConfigKey;
|
|
67
|
-
|
|
94
|
+
overlay;
|
|
95
|
+
constructor({ nangoClient, connectionId, providerConfigKey, overlay, }) {
|
|
68
96
|
const resolvedProviderConfigKey = providerConfigKey?.trim();
|
|
69
97
|
if (!resolvedProviderConfigKey) {
|
|
70
98
|
throw new Error('GitHubIntegration requires a providerConfigKey');
|
|
@@ -72,6 +100,7 @@ export class GitHubIntegration {
|
|
|
72
100
|
this.nango = nangoClient;
|
|
73
101
|
this.connectionId = connectionId;
|
|
74
102
|
this.providerConfigKey = resolvedProviderConfigKey;
|
|
103
|
+
this.overlay = overlay ?? null;
|
|
75
104
|
}
|
|
76
105
|
errorResult(action, fallback, error) {
|
|
77
106
|
const prefix = isRateLimited(error)
|
|
@@ -88,6 +117,14 @@ export class GitHubIntegration {
|
|
|
88
117
|
return this.errorResult(action, fallback, error);
|
|
89
118
|
}
|
|
90
119
|
}
|
|
120
|
+
recordOverlay(action, path, data) {
|
|
121
|
+
this.overlay?.record({
|
|
122
|
+
path,
|
|
123
|
+
data,
|
|
124
|
+
provider: SOURCE,
|
|
125
|
+
action,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
91
128
|
async searchCode(query, repo) {
|
|
92
129
|
const scopedQuery = repo ? `${query}+repo:${repo}` : query;
|
|
93
130
|
return this.execute(`searching code for "${query}"${repo ? ` in ${repo}` : ''}`, { items: [] }, async () => {
|
|
@@ -204,4 +241,82 @@ export class GitHubIntegration {
|
|
|
204
241
|
};
|
|
205
242
|
}, (data) => `Issue #${data.number}: "${data.title}" (${data.state}).`);
|
|
206
243
|
}
|
|
244
|
+
async createIssueComment(owner, repo, number, body) {
|
|
245
|
+
return this.execute(`creating issue comment in ${owner}/${repo}#${number}`, { id: 0, url: '' }, async () => {
|
|
246
|
+
const data = await this.nango.proxy({
|
|
247
|
+
connectionId: this.connectionId,
|
|
248
|
+
providerConfigKey: this.providerConfigKey,
|
|
249
|
+
method: 'POST',
|
|
250
|
+
endpoint: `/repos/${owner}/${repo}/issues/${number}/comments`,
|
|
251
|
+
data: { body },
|
|
252
|
+
});
|
|
253
|
+
const created = createdResource(data);
|
|
254
|
+
this.recordOverlay('createComment', buildIssueCommentPath(owner, repo, number, created.id), {
|
|
255
|
+
id: created.id,
|
|
256
|
+
body,
|
|
257
|
+
url: created.url,
|
|
258
|
+
issue_number: number,
|
|
259
|
+
});
|
|
260
|
+
return created;
|
|
261
|
+
}, (created) => `Created issue comment ${created.id} in ${owner}/${repo}#${number}.`);
|
|
262
|
+
}
|
|
263
|
+
async createPRComment(owner, repo, number, body, options = {}) {
|
|
264
|
+
return this.execute(`creating PR comment in ${owner}/${repo}#${number}`, { id: 0, url: '' }, async () => {
|
|
265
|
+
const data = await this.nango.proxy({
|
|
266
|
+
connectionId: this.connectionId,
|
|
267
|
+
providerConfigKey: this.providerConfigKey,
|
|
268
|
+
method: 'POST',
|
|
269
|
+
endpoint: `/repos/${owner}/${repo}/pulls/${number}/comments`,
|
|
270
|
+
data: compactRecord({
|
|
271
|
+
body,
|
|
272
|
+
commit_id: options.commitId,
|
|
273
|
+
path: options.path,
|
|
274
|
+
position: options.position,
|
|
275
|
+
line: options.line,
|
|
276
|
+
side: options.side,
|
|
277
|
+
start_line: options.startLine,
|
|
278
|
+
start_side: options.startSide,
|
|
279
|
+
subject_type: options.subjectType,
|
|
280
|
+
}),
|
|
281
|
+
});
|
|
282
|
+
const created = createdResource(data);
|
|
283
|
+
this.recordOverlay('createComment', buildPRCommentPath(owner, repo, number, created.id), {
|
|
284
|
+
id: created.id,
|
|
285
|
+
body,
|
|
286
|
+
url: created.url,
|
|
287
|
+
pull_number: number,
|
|
288
|
+
...compactRecord({
|
|
289
|
+
commit_id: options.commitId,
|
|
290
|
+
path: options.path,
|
|
291
|
+
position: options.position,
|
|
292
|
+
line: options.line,
|
|
293
|
+
side: options.side,
|
|
294
|
+
start_line: options.startLine,
|
|
295
|
+
start_side: options.startSide,
|
|
296
|
+
subject_type: options.subjectType,
|
|
297
|
+
}),
|
|
298
|
+
});
|
|
299
|
+
return created;
|
|
300
|
+
}, (created) => `Created PR comment ${created.id} in ${owner}/${repo}#${number}.`);
|
|
301
|
+
}
|
|
302
|
+
async createPRReview(owner, repo, number, body, event = 'COMMENT') {
|
|
303
|
+
return this.execute(`creating PR review (${event}) in ${owner}/${repo}#${number}`, { id: 0, url: '' }, async () => {
|
|
304
|
+
const data = await this.nango.proxy({
|
|
305
|
+
connectionId: this.connectionId,
|
|
306
|
+
providerConfigKey: this.providerConfigKey,
|
|
307
|
+
method: 'POST',
|
|
308
|
+
endpoint: `/repos/${owner}/${repo}/pulls/${number}/reviews`,
|
|
309
|
+
data: { body, event },
|
|
310
|
+
});
|
|
311
|
+
const created = createdResource(data);
|
|
312
|
+
this.recordOverlay('createReview', buildPRReviewPath(owner, repo, number, created.id), {
|
|
313
|
+
id: created.id,
|
|
314
|
+
body,
|
|
315
|
+
event,
|
|
316
|
+
url: created.url,
|
|
317
|
+
pull_number: number,
|
|
318
|
+
});
|
|
319
|
+
return created;
|
|
320
|
+
}, (created) => `Created PR review ${created.id} (${event}) in ${owner}/${repo}#${number}.`);
|
|
321
|
+
}
|
|
207
322
|
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { normalizeLinearWebhook } from "@relayfile/adapter-linear";
|
|
2
|
+
import type { NormalizedWebhook, LinearWebhookConnectionMetadata } from "@relayfile/adapter-linear";
|
|
3
|
+
import type { ProviderIngressDedup } from "./provider-ingress-dedup.js";
|
|
4
|
+
export declare function parseLinearWebhookEnvelope(rawBody: unknown): Record<string, unknown>;
|
|
5
|
+
export interface LinearIngressEvent {
|
|
6
|
+
raw: Record<string, unknown>;
|
|
7
|
+
normalized: NormalizedWebhook;
|
|
8
|
+
eventType: string;
|
|
9
|
+
objectType: string;
|
|
10
|
+
objectId: string;
|
|
11
|
+
connection: LinearWebhookConnectionMetadata;
|
|
12
|
+
organizationId: string;
|
|
13
|
+
timestamp: string;
|
|
14
|
+
}
|
|
15
|
+
export type LinearEventHandler = (event: LinearIngressEvent) => Promise<void>;
|
|
16
|
+
export { normalizeLinearWebhook };
|
|
17
|
+
export type NormalizedLinearWebhook = NormalizedWebhook;
|
|
18
|
+
export declare class LinearIngress {
|
|
19
|
+
private readonly dedup;
|
|
20
|
+
private readonly handler;
|
|
21
|
+
constructor(options: {
|
|
22
|
+
dedup: ProviderIngressDedup;
|
|
23
|
+
handler: LinearEventHandler;
|
|
24
|
+
});
|
|
25
|
+
ingest(rawBody: unknown): Promise<{
|
|
26
|
+
deduped: boolean;
|
|
27
|
+
event: LinearIngressEvent;
|
|
28
|
+
}>;
|
|
29
|
+
}
|
|
30
|
+
//# sourceMappingURL=linear-ingress.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"linear-ingress.d.ts","sourceRoot":"","sources":["../../src/integrations/linear-ingress.ts"],"names":[],"mappings":"AACA,OAAO,EACL,sBAAsB,EAMvB,MAAM,2BAA2B,CAAC;AACnC,OAAO,KAAK,EACV,iBAAiB,EACjB,+BAA+B,EAChC,MAAM,2BAA2B,CAAC;AACnC,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,6BAA6B,CAAC;AAExE,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAcpF;AAED,MAAM,WAAW,kBAAkB;IACjC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC7B,UAAU,EAAE,iBAAiB,CAAC;IAC9B,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,+BAA+B,CAAC;IAC5C,cAAc,EAAE,MAAM,CAAC;IACvB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,MAAM,kBAAkB,GAAG,CAAC,KAAK,EAAE,kBAAkB,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;AAE9E,OAAO,EAAE,sBAAsB,EAAE,CAAC;AAElC,MAAM,MAAM,uBAAuB,GAAG,iBAAiB,CAAC;AAExD,qBAAa,aAAa;IACxB,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAuB;IAC7C,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAqB;gBAEjC,OAAO,EAAE;QAAE,KAAK,EAAE,oBAAoB,CAAC;QAAC,OAAO,EAAE,kBAAkB,CAAA;KAAE;IAK3E,MAAM,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,KAAK,EAAE,kBAAkB,CAAA;KAAE,CAAC;CA0CzF"}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { parseNangoWebhookPayload } from "@relayfile/provider-nango";
|
|
2
|
+
import { normalizeLinearWebhook, parseLinearWebhookPayload as parseRawLinearWebhookPayload, extractLinearEventType, extractLinearObjectType, extractLinearObjectId, extractLinearConnectionMetadata, } from "@relayfile/adapter-linear";
|
|
3
|
+
export function parseLinearWebhookEnvelope(rawBody) {
|
|
4
|
+
const envelope = parseNangoWebhookPayload(rawBody);
|
|
5
|
+
if (envelope.type === "forward") {
|
|
6
|
+
const inner = envelope.payload;
|
|
7
|
+
if (!inner || typeof inner !== "object") {
|
|
8
|
+
throw new TypeError("Nango forward webhook payload is missing its inner object — cannot extract Linear envelope");
|
|
9
|
+
}
|
|
10
|
+
return parseRawLinearWebhookPayload(inner);
|
|
11
|
+
}
|
|
12
|
+
return parseRawLinearWebhookPayload(envelope);
|
|
13
|
+
}
|
|
14
|
+
export { normalizeLinearWebhook };
|
|
15
|
+
export class LinearIngress {
|
|
16
|
+
dedup;
|
|
17
|
+
handler;
|
|
18
|
+
constructor(options) {
|
|
19
|
+
this.dedup = options.dedup;
|
|
20
|
+
this.handler = options.handler;
|
|
21
|
+
}
|
|
22
|
+
async ingest(rawBody) {
|
|
23
|
+
const parsedPayload = parseLinearWebhookEnvelope(rawBody);
|
|
24
|
+
const normalized = normalizeLinearWebhook(parsedPayload);
|
|
25
|
+
const objectType = extractLinearObjectType(parsedPayload);
|
|
26
|
+
const objectId = extractLinearObjectId(parsedPayload);
|
|
27
|
+
const eventType = extractLinearEventType(parsedPayload);
|
|
28
|
+
const connection = extractLinearConnectionMetadata(parsedPayload);
|
|
29
|
+
const organizationId = typeof parsedPayload.organizationId === "string" ? parsedPayload.organizationId : "";
|
|
30
|
+
const timestamp = typeof parsedPayload.createdAt === "string"
|
|
31
|
+
? parsedPayload.createdAt
|
|
32
|
+
: typeof parsedPayload.updatedAt === "string"
|
|
33
|
+
? parsedPayload.updatedAt
|
|
34
|
+
: "";
|
|
35
|
+
const event = {
|
|
36
|
+
raw: parsedPayload,
|
|
37
|
+
normalized,
|
|
38
|
+
eventType,
|
|
39
|
+
objectType,
|
|
40
|
+
objectId,
|
|
41
|
+
connection,
|
|
42
|
+
organizationId,
|
|
43
|
+
timestamp,
|
|
44
|
+
};
|
|
45
|
+
const dedupKey = `linear:${organizationId}:${normalized.eventType}:${normalized.objectId}:${timestamp}`;
|
|
46
|
+
if (this.dedup.has(dedupKey)) {
|
|
47
|
+
return { deduped: true, event };
|
|
48
|
+
}
|
|
49
|
+
this.dedup.set(dedupKey);
|
|
50
|
+
try {
|
|
51
|
+
await this.handler(event);
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
console.error("[linear-ingress] Handler error", error);
|
|
55
|
+
}
|
|
56
|
+
return { deduped: false, event };
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { ProviderIngressDedup } from "./provider-ingress-dedup.js";
|
|
2
|
+
export declare function parseNotionWebhookEnvelope(rawBody: unknown): Record<string, unknown>;
|
|
3
|
+
export interface NotionIngressEvent {
|
|
4
|
+
raw: Record<string, unknown>;
|
|
5
|
+
eventType: string;
|
|
6
|
+
objectType: string;
|
|
7
|
+
objectId: string;
|
|
8
|
+
action: string | null;
|
|
9
|
+
workspaceId: string;
|
|
10
|
+
timestamp: string;
|
|
11
|
+
}
|
|
12
|
+
export type NotionEventHandler = (event: NotionIngressEvent) => Promise<void>;
|
|
13
|
+
export declare function buildNotionIngressEvent(rawPayload: Record<string, unknown>): NotionIngressEvent;
|
|
14
|
+
export declare class NotionIngress {
|
|
15
|
+
private readonly dedup;
|
|
16
|
+
private readonly handler;
|
|
17
|
+
constructor(options: {
|
|
18
|
+
dedup: ProviderIngressDedup;
|
|
19
|
+
handler: NotionEventHandler;
|
|
20
|
+
});
|
|
21
|
+
ingest(rawBody: unknown): Promise<{
|
|
22
|
+
deduped: boolean;
|
|
23
|
+
event: NotionIngressEvent;
|
|
24
|
+
}>;
|
|
25
|
+
}
|
|
26
|
+
//# sourceMappingURL=notion-ingress.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"notion-ingress.d.ts","sourceRoot":"","sources":["../../src/integrations/notion-ingress.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,6BAA6B,CAAC;AAoBxE,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAkBpF;AAED,MAAM,WAAW,kBAAkB;IACjC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC7B,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,MAAM,kBAAkB,GAAG,CAAC,KAAK,EAAE,kBAAkB,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;AAE9E,wBAAgB,uBAAuB,CACrC,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAClC,kBAAkB,CAkBpB;AAED,qBAAa,aAAa;IACxB,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAuB;IAC7C,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAqB;gBAEjC,OAAO,EAAE;QAAE,KAAK,EAAE,oBAAoB,CAAC;QAAC,OAAO,EAAE,kBAAkB,CAAA;KAAE;IAK3E,MAAM,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,KAAK,EAAE,kBAAkB,CAAA;KAAE,CAAC;CAmBzF"}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { extractForwardMetadata, parseNangoWebhookPayload, } from "@relayfile/provider-nango";
|
|
2
|
+
function isRecord(value) {
|
|
3
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4
|
+
}
|
|
5
|
+
function readNonEmpty(value) {
|
|
6
|
+
return value && value.length > 0 ? value : undefined;
|
|
7
|
+
}
|
|
8
|
+
function tryExtractForwardMetadata(rawPayload) {
|
|
9
|
+
try {
|
|
10
|
+
return extractForwardMetadata(rawPayload);
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
export function parseNotionWebhookEnvelope(rawBody) {
|
|
17
|
+
const envelope = parseNangoWebhookPayload(rawBody);
|
|
18
|
+
if (envelope.type === "forward") {
|
|
19
|
+
const inner = envelope.payload;
|
|
20
|
+
if (!inner || typeof inner !== "object") {
|
|
21
|
+
throw new TypeError("Nango forward webhook payload is missing its inner object — cannot extract Notion envelope");
|
|
22
|
+
}
|
|
23
|
+
return inner;
|
|
24
|
+
}
|
|
25
|
+
if (!isRecord(envelope)) {
|
|
26
|
+
throw new TypeError("Notion webhook payload must be a JSON object");
|
|
27
|
+
}
|
|
28
|
+
return envelope;
|
|
29
|
+
}
|
|
30
|
+
export function buildNotionIngressEvent(rawPayload) {
|
|
31
|
+
const metadata = tryExtractForwardMetadata(rawPayload);
|
|
32
|
+
const eventType = readNonEmpty(metadata?.eventType) ?? String(rawPayload.type ?? "unknown");
|
|
33
|
+
const objectType = readNonEmpty(metadata?.objectType) ?? String(rawPayload.object ?? "unknown");
|
|
34
|
+
const objectId = readNonEmpty(metadata?.objectId) ?? String(rawPayload.id ?? "");
|
|
35
|
+
const workspaceId = String(rawPayload.workspace_id ?? "");
|
|
36
|
+
const timestamp = String(rawPayload.last_edited_time ?? rawPayload.created_time ?? "");
|
|
37
|
+
return {
|
|
38
|
+
raw: rawPayload,
|
|
39
|
+
eventType,
|
|
40
|
+
objectType,
|
|
41
|
+
objectId,
|
|
42
|
+
action: metadata?.action ?? null,
|
|
43
|
+
workspaceId,
|
|
44
|
+
timestamp,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
export class NotionIngress {
|
|
48
|
+
dedup;
|
|
49
|
+
handler;
|
|
50
|
+
constructor(options) {
|
|
51
|
+
this.dedup = options.dedup;
|
|
52
|
+
this.handler = options.handler;
|
|
53
|
+
}
|
|
54
|
+
async ingest(rawBody) {
|
|
55
|
+
const parsedPayload = parseNotionWebhookEnvelope(rawBody);
|
|
56
|
+
const event = buildNotionIngressEvent(parsedPayload);
|
|
57
|
+
const dedupKey = `notion:${event.workspaceId}:${event.eventType}:${event.objectId}:${event.timestamp}`;
|
|
58
|
+
if (this.dedup.has(dedupKey)) {
|
|
59
|
+
return { deduped: true, event };
|
|
60
|
+
}
|
|
61
|
+
this.dedup.set(dedupKey);
|
|
62
|
+
try {
|
|
63
|
+
await this.handler(event);
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
console.error("[notion-ingress] Handler error", error);
|
|
67
|
+
}
|
|
68
|
+
return { deduped: false, event };
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export declare class ProviderIngressDedup {
|
|
2
|
+
private readonly ttlMs;
|
|
3
|
+
private readonly maxEntries;
|
|
4
|
+
private readonly store;
|
|
5
|
+
constructor(options?: {
|
|
6
|
+
ttlMs?: number;
|
|
7
|
+
maxEntries?: number;
|
|
8
|
+
});
|
|
9
|
+
has(key: string): boolean;
|
|
10
|
+
set(key: string, ttlMs?: number): void;
|
|
11
|
+
size(): number;
|
|
12
|
+
clear(): void;
|
|
13
|
+
}
|
|
14
|
+
//# sourceMappingURL=provider-ingress-dedup.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"provider-ingress-dedup.d.ts","sourceRoot":"","sources":["../../src/integrations/provider-ingress-dedup.ts"],"names":[],"mappings":"AAAA,qBAAa,oBAAoB;IAC/B,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAS;IAC/B,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAA6B;gBAEvC,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,UAAU,CAAC,EAAE,MAAM,CAAA;KAAE;IAK7D,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO;IAezB,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI;IAYtC,IAAI,IAAI,MAAM;IAId,KAAK,IAAI,IAAI;CAGd"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export class ProviderIngressDedup {
|
|
2
|
+
ttlMs;
|
|
3
|
+
maxEntries;
|
|
4
|
+
store = new Map();
|
|
5
|
+
constructor(options) {
|
|
6
|
+
this.ttlMs = options?.ttlMs ?? 600_000;
|
|
7
|
+
this.maxEntries = options?.maxEntries ?? 2000;
|
|
8
|
+
}
|
|
9
|
+
has(key) {
|
|
10
|
+
const expiry = this.store.get(key);
|
|
11
|
+
if (expiry === undefined) {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
if (expiry <= Date.now()) {
|
|
15
|
+
this.store.delete(key);
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
set(key, ttlMs) {
|
|
21
|
+
this.store.set(key, Date.now() + (ttlMs ?? this.ttlMs));
|
|
22
|
+
if (this.store.size > this.maxEntries) {
|
|
23
|
+
const oldestKey = this.store.keys().next().value;
|
|
24
|
+
if (oldestKey !== undefined) {
|
|
25
|
+
this.store.delete(oldestKey);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
size() {
|
|
30
|
+
return this.store.size;
|
|
31
|
+
}
|
|
32
|
+
clear() {
|
|
33
|
+
this.store.clear();
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"provider-ingress-dedup.test.d.ts","sourceRoot":"","sources":["../../src/integrations/provider-ingress-dedup.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { ProviderIngressDedup } from "./provider-ingress-dedup.js";
|
|
3
|
+
describe("ProviderIngressDedup", () => {
|
|
4
|
+
const now = new Date("2026-04-15T12:00:00.000Z");
|
|
5
|
+
beforeEach(() => {
|
|
6
|
+
vi.useFakeTimers();
|
|
7
|
+
vi.setSystemTime(now);
|
|
8
|
+
});
|
|
9
|
+
afterEach(() => {
|
|
10
|
+
vi.useRealTimers();
|
|
11
|
+
});
|
|
12
|
+
it("returns false for unknown keys", () => {
|
|
13
|
+
const dedup = new ProviderIngressDedup();
|
|
14
|
+
expect(dedup.has("linear:event:missing")).toBe(false);
|
|
15
|
+
});
|
|
16
|
+
it("round-trips set keys through has", () => {
|
|
17
|
+
const dedup = new ProviderIngressDedup();
|
|
18
|
+
dedup.set("linear:event:created");
|
|
19
|
+
expect(dedup.has("linear:event:created")).toBe(true);
|
|
20
|
+
});
|
|
21
|
+
it("returns false for expired entries after ttlMs passes", () => {
|
|
22
|
+
const dedup = new ProviderIngressDedup({ ttlMs: 1_000 });
|
|
23
|
+
dedup.set("linear:event:expired");
|
|
24
|
+
vi.advanceTimersByTime(1_001);
|
|
25
|
+
expect(dedup.has("linear:event:expired")).toBe(false);
|
|
26
|
+
expect(dedup.size()).toBe(0);
|
|
27
|
+
});
|
|
28
|
+
it("evicts the least recently used entry when maxEntries is exceeded", () => {
|
|
29
|
+
const dedup = new ProviderIngressDedup({ maxEntries: 2 });
|
|
30
|
+
dedup.set("linear:event:oldest");
|
|
31
|
+
dedup.set("linear:event:middle");
|
|
32
|
+
dedup.set("linear:event:newest");
|
|
33
|
+
expect(dedup.has("linear:event:oldest")).toBe(false);
|
|
34
|
+
expect(dedup.has("linear:event:middle")).toBe(true);
|
|
35
|
+
expect(dedup.has("linear:event:newest")).toBe(true);
|
|
36
|
+
expect(dedup.size()).toBe(2);
|
|
37
|
+
});
|
|
38
|
+
it("clear empties stored entries", () => {
|
|
39
|
+
const dedup = new ProviderIngressDedup();
|
|
40
|
+
dedup.set("linear:event:first");
|
|
41
|
+
dedup.set("linear:event:second");
|
|
42
|
+
dedup.clear();
|
|
43
|
+
expect(dedup.has("linear:event:first")).toBe(false);
|
|
44
|
+
expect(dedup.has("linear:event:second")).toBe(false);
|
|
45
|
+
expect(dedup.size()).toBe(0);
|
|
46
|
+
});
|
|
47
|
+
it("size reflects the stored entry count", () => {
|
|
48
|
+
const dedup = new ProviderIngressDedup();
|
|
49
|
+
expect(dedup.size()).toBe(0);
|
|
50
|
+
dedup.set("linear:event:first");
|
|
51
|
+
expect(dedup.size()).toBe(1);
|
|
52
|
+
dedup.set("linear:event:second");
|
|
53
|
+
expect(dedup.size()).toBe(2);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { RecentActionsOverlay } from "./recent-actions-overlay.js";
|
|
2
|
+
export type WriteMode = "sync" | "fire-and-forget";
|
|
3
|
+
export type WriteActionState = "pending" | "success" | "failed";
|
|
4
|
+
export interface WriteAction {
|
|
5
|
+
kind: "slack:postMessage" | "slack:addReaction" | "github:createComment" | "github:createReview" | string;
|
|
6
|
+
channel?: string;
|
|
7
|
+
threadTs?: string;
|
|
8
|
+
text?: string;
|
|
9
|
+
owner?: string;
|
|
10
|
+
repo?: string;
|
|
11
|
+
number?: number;
|
|
12
|
+
body?: string;
|
|
13
|
+
emoji?: string;
|
|
14
|
+
[key: string]: unknown;
|
|
15
|
+
}
|
|
16
|
+
export interface WriteResult {
|
|
17
|
+
mode: WriteMode;
|
|
18
|
+
ok: boolean;
|
|
19
|
+
data?: Record<string, unknown>;
|
|
20
|
+
error?: string;
|
|
21
|
+
actionId: string;
|
|
22
|
+
providerPath: string;
|
|
23
|
+
}
|
|
24
|
+
export interface WriteStatus {
|
|
25
|
+
actionId: string;
|
|
26
|
+
state: WriteActionState;
|
|
27
|
+
result?: WriteResult;
|
|
28
|
+
createdAt: number;
|
|
29
|
+
}
|
|
30
|
+
export interface SlackPostMessageResult {
|
|
31
|
+
ok: boolean;
|
|
32
|
+
ts?: string;
|
|
33
|
+
error?: string;
|
|
34
|
+
}
|
|
35
|
+
export interface SlackEgress {
|
|
36
|
+
postMessage(channel: string, text: string, threadTs?: string): Promise<SlackPostMessageResult>;
|
|
37
|
+
addReaction(channel: string, timestamp: string, emoji: string): Promise<void>;
|
|
38
|
+
}
|
|
39
|
+
export type GitHubReviewEvent = "APPROVE" | "REQUEST_CHANGES" | "COMMENT";
|
|
40
|
+
export interface GitHubCreatedResource {
|
|
41
|
+
id: number;
|
|
42
|
+
url: string;
|
|
43
|
+
}
|
|
44
|
+
export interface IntegrationResultLike<T> {
|
|
45
|
+
data: T;
|
|
46
|
+
}
|
|
47
|
+
export interface GitHubIntegration {
|
|
48
|
+
createIssueComment(owner: string, repo: string, number: number, body: string): Promise<IntegrationResultLike<GitHubCreatedResource> | GitHubCreatedResource>;
|
|
49
|
+
createPRReview(owner: string, repo: string, number: number, body: string, event?: GitHubReviewEvent): Promise<IntegrationResultLike<GitHubCreatedResource> | GitHubCreatedResource>;
|
|
50
|
+
}
|
|
51
|
+
export interface ProviderWriteFacadeOptions {
|
|
52
|
+
slackEgress?: SlackEgress;
|
|
53
|
+
githubIntegration?: GitHubIntegration;
|
|
54
|
+
overlay?: RecentActionsOverlay;
|
|
55
|
+
defaultMode?: WriteMode;
|
|
56
|
+
}
|
|
57
|
+
export declare const DEFAULT_WRITE_MODE: WriteMode;
|
|
58
|
+
export declare const STATUS_RETENTION_MS = 300000;
|
|
59
|
+
export declare class ProviderWriteFacade {
|
|
60
|
+
private readonly slackEgress;
|
|
61
|
+
private readonly githubIntegration;
|
|
62
|
+
private readonly overlay;
|
|
63
|
+
private readonly defaultMode;
|
|
64
|
+
private readonly statuses;
|
|
65
|
+
constructor(options: ProviderWriteFacadeOptions);
|
|
66
|
+
write(action: WriteAction, mode?: WriteMode): Promise<WriteResult>;
|
|
67
|
+
getStatus(actionId: string): WriteStatus | null;
|
|
68
|
+
private writeSync;
|
|
69
|
+
private writeFireAndForget;
|
|
70
|
+
private createDispatchPlan;
|
|
71
|
+
private createSlackPostMessagePlan;
|
|
72
|
+
private createSlackAddReactionPlan;
|
|
73
|
+
private createGitHubCreateCommentPlan;
|
|
74
|
+
private createGitHubCreateReviewPlan;
|
|
75
|
+
private requireSlackEgress;
|
|
76
|
+
private requireGitHubIntegration;
|
|
77
|
+
private recordOverlay;
|
|
78
|
+
private pruneCompletedStatuses;
|
|
79
|
+
}
|
|
80
|
+
//# sourceMappingURL=provider-write-facade.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"provider-write-facade.d.ts","sourceRoot":"","sources":["../../src/integrations/provider-write-facade.ts"],"names":[],"mappings":"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"}
|