@wolpertingerlabs/drawlatch 1.0.0-alpha.10.0 → 1.0.0-alpha.15.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/bin/drawlatch.js +169 -22
- package/dist/connections/developer-tools/github.json +58 -4
- package/dist/remote/ingestors/e2e/setup.d.ts +69 -0
- package/dist/remote/ingestors/e2e/setup.js +147 -0
- package/dist/remote/ingestors/manager.d.ts +19 -0
- package/dist/remote/ingestors/manager.js +75 -0
- package/dist/remote/ingestors/poll/poll-ingestor.d.ts +2 -0
- package/dist/remote/ingestors/poll/poll-ingestor.js +21 -0
- package/dist/remote/ingestors/types.d.ts +6 -0
- package/dist/remote/ingestors/webhook/github-webhook-ingestor.d.ts +16 -0
- package/dist/remote/ingestors/webhook/github-webhook-ingestor.js +49 -3
- package/dist/remote/ingestors/webhook/webhook-lifecycle-manager.js +6 -6
- package/dist/remote/server.js +59 -1
- package/dist/remote/triggers/rule-engine.d.ts +40 -0
- package/dist/remote/triggers/rule-engine.js +228 -0
- package/dist/remote/triggers/types.d.ts +69 -0
- package/dist/remote/triggers/types.js +10 -0
- package/dist/shared/config.d.ts +5 -0
- package/package.json +2 -1
package/bin/drawlatch.js
CHANGED
|
@@ -81,6 +81,7 @@ try {
|
|
|
81
81
|
follow: { type: "boolean", default: false },
|
|
82
82
|
path: { type: "boolean", default: false },
|
|
83
83
|
full: { type: "boolean", default: false },
|
|
84
|
+
requests: { type: "boolean", default: false },
|
|
84
85
|
ttl: { type: "string", default: "300" },
|
|
85
86
|
},
|
|
86
87
|
strict: false,
|
|
@@ -185,6 +186,13 @@ switch (subcommand) {
|
|
|
185
186
|
await cmdSync();
|
|
186
187
|
}
|
|
187
188
|
break;
|
|
189
|
+
case "watch":
|
|
190
|
+
if (values.help) {
|
|
191
|
+
printWatchHelp();
|
|
192
|
+
} else {
|
|
193
|
+
await cmdWatch();
|
|
194
|
+
}
|
|
195
|
+
break;
|
|
188
196
|
case "help":
|
|
189
197
|
printHelp();
|
|
190
198
|
{
|
|
@@ -477,39 +485,154 @@ async function cmdLogs() {
|
|
|
477
485
|
|
|
478
486
|
const lines = parseInt(values.lines, 10) || 50;
|
|
479
487
|
const follow = values.follow;
|
|
488
|
+
const showRequests = values.requests;
|
|
480
489
|
|
|
481
490
|
const tailArgs = follow
|
|
482
491
|
? ["-n", String(lines), "-f", LOG_FILE]
|
|
483
492
|
: ["-n", String(lines), LOG_FILE];
|
|
484
493
|
|
|
485
|
-
|
|
494
|
+
if (showRequests) {
|
|
495
|
+
// Show everything — pipe directly to stdout
|
|
496
|
+
const tail = spawn("tail", tailArgs, { stdio: "inherit" });
|
|
486
497
|
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
498
|
+
tail.on("error", () => logsFallback(lines, follow, null));
|
|
499
|
+
|
|
500
|
+
process.on("SIGINT", () => {
|
|
501
|
+
tail.kill();
|
|
502
|
+
process.exit(0);
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
await new Promise((res) => tail.on("close", res));
|
|
506
|
+
} else {
|
|
507
|
+
// Filter out [audit] lines (request/response noise from poll_events etc.)
|
|
508
|
+
const tail = spawn("tail", tailArgs, { stdio: ["ignore", "pipe", "inherit"] });
|
|
509
|
+
const grepProc = spawn("grep", ["-v", "^\\[audit\\]"], {
|
|
510
|
+
stdio: [tail.stdout, "inherit", "inherit"],
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
tail.on("error", () => logsFallback(lines, follow, "[audit]"));
|
|
514
|
+
|
|
515
|
+
process.on("SIGINT", () => {
|
|
516
|
+
tail.kill();
|
|
517
|
+
grepProc.kill();
|
|
518
|
+
process.exit(0);
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
await new Promise((res) => grepProc.on("close", res));
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
function logsFallback(lines, follow, filterPrefix) {
|
|
526
|
+
try {
|
|
527
|
+
const content = readFileSync(LOG_FILE, "utf-8");
|
|
528
|
+
let allLines = content.split("\n");
|
|
529
|
+
if (filterPrefix) {
|
|
530
|
+
allLines = allLines.filter((l) => !l.startsWith(filterPrefix));
|
|
502
531
|
}
|
|
503
|
-
|
|
532
|
+
const lastLines = allLines.slice(-lines).join("\n");
|
|
533
|
+
console.log(lastLines);
|
|
534
|
+
if (follow) {
|
|
535
|
+
console.log(
|
|
536
|
+
"\n(Live following not available \u2014 'tail' command not found)",
|
|
537
|
+
);
|
|
538
|
+
}
|
|
539
|
+
} catch (err) {
|
|
540
|
+
console.error(`Error reading log file: ${err.message}`);
|
|
541
|
+
process.exit(1);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
504
544
|
|
|
505
|
-
|
|
545
|
+
async function cmdWatch() {
|
|
546
|
+
const config = loadRemoteConfig();
|
|
547
|
+
const port = config.port;
|
|
548
|
+
const host = config.host;
|
|
549
|
+
|
|
550
|
+
// Verify the server is running
|
|
551
|
+
const healthy = await healthCheck(host, port);
|
|
552
|
+
if (!healthy) {
|
|
553
|
+
console.error("Remote server is not running. Start it first:");
|
|
554
|
+
console.error(" drawlatch start");
|
|
555
|
+
process.exit(1);
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
const showFull = values.full;
|
|
559
|
+
const sourceFilter = positionals[0] || null;
|
|
560
|
+
|
|
561
|
+
console.log("Watching for events" + (sourceFilter ? ` from "${sourceFilter}"` : "") + "...");
|
|
562
|
+
console.log("Press Ctrl+C to stop.\n");
|
|
563
|
+
|
|
564
|
+
const controller = new AbortController();
|
|
506
565
|
process.on("SIGINT", () => {
|
|
507
|
-
|
|
566
|
+
controller.abort();
|
|
508
567
|
process.exit(0);
|
|
509
568
|
});
|
|
510
569
|
|
|
511
|
-
|
|
512
|
-
|
|
570
|
+
try {
|
|
571
|
+
const res = await fetch(
|
|
572
|
+
`http://${connectHost(host)}:${port}/events/stream`,
|
|
573
|
+
{ signal: controller.signal },
|
|
574
|
+
);
|
|
575
|
+
|
|
576
|
+
if (!res.ok) {
|
|
577
|
+
console.error(`Failed to connect to event stream: HTTP ${res.status}`);
|
|
578
|
+
process.exit(1);
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
const decoder = new TextDecoder();
|
|
582
|
+
let buffer = "";
|
|
583
|
+
|
|
584
|
+
for await (const chunk of res.body) {
|
|
585
|
+
buffer += decoder.decode(chunk, { stream: true });
|
|
586
|
+
|
|
587
|
+
// Parse SSE lines
|
|
588
|
+
let newlineIdx;
|
|
589
|
+
while ((newlineIdx = buffer.indexOf("\n\n")) !== -1) {
|
|
590
|
+
const message = buffer.slice(0, newlineIdx);
|
|
591
|
+
buffer = buffer.slice(newlineIdx + 2);
|
|
592
|
+
|
|
593
|
+
for (const line of message.split("\n")) {
|
|
594
|
+
if (!line.startsWith("data: ")) continue;
|
|
595
|
+
|
|
596
|
+
try {
|
|
597
|
+
const event = JSON.parse(line.slice(6));
|
|
598
|
+
|
|
599
|
+
// Apply source filter if set
|
|
600
|
+
if (sourceFilter && event.source !== sourceFilter) continue;
|
|
601
|
+
|
|
602
|
+
const time = new Date(event.receivedAt).toLocaleTimeString();
|
|
603
|
+
const source = event.source;
|
|
604
|
+
const instance = event.instanceId ? `:${event.instanceId}` : "";
|
|
605
|
+
const caller = event.callerAlias || "?";
|
|
606
|
+
const eventType = event.eventType;
|
|
607
|
+
|
|
608
|
+
const dataStr = JSON.stringify(event.data);
|
|
609
|
+
|
|
610
|
+
if (showFull) {
|
|
611
|
+
console.log(
|
|
612
|
+
`\x1b[2m${time}\x1b[0m \x1b[36m${source}${instance}\x1b[0m \x1b[33m${eventType}\x1b[0m \x1b[2m(${caller})\x1b[0m`
|
|
613
|
+
);
|
|
614
|
+
console.log(dataStr);
|
|
615
|
+
console.log("");
|
|
616
|
+
} else {
|
|
617
|
+
const preview =
|
|
618
|
+
dataStr.length > 100
|
|
619
|
+
? dataStr.slice(0, 100) + "…"
|
|
620
|
+
: dataStr;
|
|
621
|
+
console.log(
|
|
622
|
+
`\x1b[2m${time}\x1b[0m \x1b[36m${source}${instance}\x1b[0m \x1b[33m${eventType}\x1b[0m \x1b[2m(${caller})\x1b[0m ${preview}`
|
|
623
|
+
);
|
|
624
|
+
}
|
|
625
|
+
} catch {
|
|
626
|
+
// Skip malformed SSE data
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
} catch (err) {
|
|
632
|
+
if (err.name === "AbortError") return;
|
|
633
|
+
console.error(`Event stream error: ${err.message}`);
|
|
634
|
+
process.exit(1);
|
|
635
|
+
}
|
|
513
636
|
}
|
|
514
637
|
|
|
515
638
|
function cmdConfig() {
|
|
@@ -1126,6 +1249,7 @@ Commands:
|
|
|
1126
1249
|
status Show server status (PID, port, uptime, health, sessions)
|
|
1127
1250
|
logs View and follow remote server logs
|
|
1128
1251
|
config Show effective configuration
|
|
1252
|
+
watch Watch ingestor events in real time
|
|
1129
1253
|
doctor Validate setup and diagnose issues
|
|
1130
1254
|
generate-keys Generate Ed25519 + X25519 keypairs
|
|
1131
1255
|
sync Exchange keys with a callboard instance
|
|
@@ -1220,19 +1344,42 @@ function printLogsHelp() {
|
|
|
1220
1344
|
console.log(`
|
|
1221
1345
|
drawlatch logs
|
|
1222
1346
|
|
|
1223
|
-
View server logs.
|
|
1347
|
+
View server logs (request/response audit lines are hidden by default).
|
|
1224
1348
|
|
|
1225
1349
|
Usage: drawlatch logs [options]
|
|
1226
1350
|
|
|
1227
1351
|
Options:
|
|
1228
1352
|
-n, --lines <number> Number of lines to show (default: 50)
|
|
1229
1353
|
--follow Follow/tail the log output (default: print and exit)
|
|
1354
|
+
--requests Include request/response audit lines (noisy with polling)
|
|
1230
1355
|
-h, --help Show this help message
|
|
1231
1356
|
|
|
1232
1357
|
Log file: ~/.drawlatch/logs/drawlatch.log
|
|
1233
1358
|
`);
|
|
1234
1359
|
}
|
|
1235
1360
|
|
|
1361
|
+
function printWatchHelp() {
|
|
1362
|
+
console.log(`
|
|
1363
|
+
drawlatch watch
|
|
1364
|
+
|
|
1365
|
+
Watch ingestor events in real time.
|
|
1366
|
+
|
|
1367
|
+
Usage: drawlatch watch [source] [options]
|
|
1368
|
+
|
|
1369
|
+
Arguments:
|
|
1370
|
+
source Filter to a specific connection (e.g., "discord-bot", "github")
|
|
1371
|
+
|
|
1372
|
+
Options:
|
|
1373
|
+
--full Show full event payload (default: truncate to 100 chars)
|
|
1374
|
+
-h, --help Show this help message
|
|
1375
|
+
|
|
1376
|
+
Examples:
|
|
1377
|
+
drawlatch watch Watch all events
|
|
1378
|
+
drawlatch watch github Watch only GitHub events
|
|
1379
|
+
drawlatch watch discord-bot --full Watch Discord events with full payloads
|
|
1380
|
+
`);
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1236
1383
|
function printConfigHelp() {
|
|
1237
1384
|
console.log(`
|
|
1238
1385
|
drawlatch config
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "GitHub API",
|
|
3
|
-
"stability": "
|
|
3
|
+
"stability": "stable",
|
|
4
4
|
"category": "developer-tools",
|
|
5
5
|
"description": "GitHub REST API — repositories, issues, pull requests, users, organizations, and more. Auth is handled automatically via the GITHUB_TOKEN environment variable. Includes a webhook ingestor for real-time events (push, pull_request, issues, etc.) — set GITHUB_WEBHOOK_SECRET and use poll_events to retrieve them.",
|
|
6
6
|
"docsUrl": "https://docs.github.com/en/rest",
|
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
},
|
|
14
14
|
"secrets": {
|
|
15
15
|
"GITHUB_TOKEN": "${GITHUB_TOKEN}",
|
|
16
|
-
"GITHUB_WEBHOOK_SECRET": "${GITHUB_WEBHOOK_SECRET}"
|
|
16
|
+
"GITHUB_WEBHOOK_SECRET": "${GITHUB_WEBHOOK_SECRET}",
|
|
17
|
+
"GITHUB_WEBHOOK_URL": "${GITHUB_WEBHOOK_URL}"
|
|
17
18
|
},
|
|
18
19
|
"allowedEndpoints": ["https://api.github.com/**"],
|
|
19
20
|
"ingestor": {
|
|
@@ -21,7 +22,51 @@
|
|
|
21
22
|
"webhook": {
|
|
22
23
|
"path": "github",
|
|
23
24
|
"signatureHeader": "X-Hub-Signature-256",
|
|
24
|
-
"signatureSecret": "GITHUB_WEBHOOK_SECRET"
|
|
25
|
+
"signatureSecret": "GITHUB_WEBHOOK_SECRET",
|
|
26
|
+
"callbackUrl": "${GITHUB_WEBHOOK_URL}",
|
|
27
|
+
"lifecycle": {
|
|
28
|
+
"list": {
|
|
29
|
+
"method": "GET",
|
|
30
|
+
"url": "https://api.github.com/repos/${repoFilter}/hooks",
|
|
31
|
+
"headers": {
|
|
32
|
+
"Authorization": "Bearer ${GITHUB_TOKEN}",
|
|
33
|
+
"Accept": "application/vnd.github+json",
|
|
34
|
+
"X-GitHub-Api-Version": "2022-11-28"
|
|
35
|
+
},
|
|
36
|
+
"callbackUrlField": "config.url",
|
|
37
|
+
"idField": "id"
|
|
38
|
+
},
|
|
39
|
+
"register": {
|
|
40
|
+
"method": "POST",
|
|
41
|
+
"url": "https://api.github.com/repos/${repoFilter}/hooks",
|
|
42
|
+
"headers": {
|
|
43
|
+
"Authorization": "Bearer ${GITHUB_TOKEN}",
|
|
44
|
+
"Accept": "application/vnd.github+json",
|
|
45
|
+
"X-GitHub-Api-Version": "2022-11-28"
|
|
46
|
+
},
|
|
47
|
+
"body": {
|
|
48
|
+
"name": "web",
|
|
49
|
+
"active": true,
|
|
50
|
+
"events": ["push", "pull_request", "issues", "issue_comment", "create", "delete", "release", "workflow_run", "check_run"],
|
|
51
|
+
"config": {
|
|
52
|
+
"url": "${GITHUB_WEBHOOK_URL}",
|
|
53
|
+
"content_type": "json",
|
|
54
|
+
"secret": "${GITHUB_WEBHOOK_SECRET}",
|
|
55
|
+
"insecure_ssl": "0"
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
"idField": "id"
|
|
59
|
+
},
|
|
60
|
+
"unregister": {
|
|
61
|
+
"method": "DELETE",
|
|
62
|
+
"url": "https://api.github.com/repos/${repoFilter}/hooks/${_webhookId}",
|
|
63
|
+
"headers": {
|
|
64
|
+
"Authorization": "Bearer ${GITHUB_TOKEN}",
|
|
65
|
+
"Accept": "application/vnd.github+json",
|
|
66
|
+
"X-GitHub-Api-Version": "2022-11-28"
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
25
70
|
}
|
|
26
71
|
},
|
|
27
72
|
"testIngestor": {
|
|
@@ -37,12 +82,21 @@
|
|
|
37
82
|
{
|
|
38
83
|
"key": "repoFilter",
|
|
39
84
|
"label": "Repository Filter",
|
|
40
|
-
"description": "
|
|
85
|
+
"description": "Register a repo-level webhook and only capture events from this repository (owner/repo format). Mutually exclusive with Organization for registration.",
|
|
41
86
|
"type": "text[]",
|
|
42
87
|
"instanceKey": true,
|
|
43
88
|
"placeholder": "e.g., octocat/Hello-World",
|
|
44
89
|
"group": "Filtering"
|
|
45
90
|
},
|
|
91
|
+
{
|
|
92
|
+
"key": "orgFilter",
|
|
93
|
+
"label": "Organization",
|
|
94
|
+
"description": "Register an org-level webhook that receives events from all repos in the organization. Mutually exclusive with Repository Filter for registration.",
|
|
95
|
+
"type": "text",
|
|
96
|
+
"instanceKey": true,
|
|
97
|
+
"placeholder": "e.g., my-org",
|
|
98
|
+
"group": "Filtering"
|
|
99
|
+
},
|
|
46
100
|
{
|
|
47
101
|
"key": "eventFilter",
|
|
48
102
|
"label": "Event Types",
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared E2E test setup for connection ingestor tests.
|
|
3
|
+
*
|
|
4
|
+
* Provides helpers to:
|
|
5
|
+
* - Load .env.e2e environment variables
|
|
6
|
+
* - Build a RemoteServerConfig with in-memory keys and requested connections
|
|
7
|
+
* - Boot an Express server on a random port
|
|
8
|
+
* - Generate valid webhook signatures (GitHub, Trello)
|
|
9
|
+
* - Wait for ingestor state transitions and event arrival
|
|
10
|
+
*/
|
|
11
|
+
import type { Server } from 'node:http';
|
|
12
|
+
import { IngestorManager } from '../manager.js';
|
|
13
|
+
import type { RemoteServerConfig } from '../../../shared/config.js';
|
|
14
|
+
import type { IngestedEvent, IngestorState } from '../types.js';
|
|
15
|
+
export declare function loadE2EEnv(): void;
|
|
16
|
+
/**
|
|
17
|
+
* Check that all required env vars are set.
|
|
18
|
+
* Returns the missing var names (empty array = all present).
|
|
19
|
+
*/
|
|
20
|
+
export declare function checkEnvVars(vars: string[]): string[];
|
|
21
|
+
/**
|
|
22
|
+
* Build a RemoteServerConfig for E2E tests.
|
|
23
|
+
*
|
|
24
|
+
* Creates a single caller ("e2e-client") with the requested connections.
|
|
25
|
+
* Env vars are injected into caller.env so secret resolution works against
|
|
26
|
+
* process.env (already loaded by loadE2EEnv).
|
|
27
|
+
*/
|
|
28
|
+
export declare function buildE2EConfig(connections: string[], opts?: {
|
|
29
|
+
env?: Record<string, string>;
|
|
30
|
+
ingestorOverrides?: Record<string, Record<string, unknown>>;
|
|
31
|
+
}): RemoteServerConfig;
|
|
32
|
+
export interface E2EServer {
|
|
33
|
+
server: Server;
|
|
34
|
+
baseUrl: string;
|
|
35
|
+
ingestorManager: IngestorManager;
|
|
36
|
+
/** Gracefully shut down server and stop all ingestors. */
|
|
37
|
+
teardown: () => Promise<void>;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Boot an Express server with the given config on a random port.
|
|
41
|
+
* Starts all ingestors and returns handles for testing.
|
|
42
|
+
*/
|
|
43
|
+
export declare function bootServer(config: RemoteServerConfig): Promise<E2EServer>;
|
|
44
|
+
/** Generate a valid GitHub HMAC-SHA256 signature for a raw body. */
|
|
45
|
+
export declare function signGitHubPayload(rawBody: string | Buffer, secret: string): string;
|
|
46
|
+
/** Generate a valid Trello HMAC-SHA1 base64 signature for body + callbackUrl. */
|
|
47
|
+
export declare function signTrelloPayload(rawBody: string | Buffer, callbackUrl: string, secret: string): string;
|
|
48
|
+
/**
|
|
49
|
+
* Wait for an ingestor to reach a specific state.
|
|
50
|
+
* Polls every 250ms until the timeout is reached.
|
|
51
|
+
*/
|
|
52
|
+
export declare function waitForIngestorState(manager: IngestorManager, callerAlias: string, connection: string, targetState: IngestorState, timeoutMs?: number): Promise<void>;
|
|
53
|
+
/**
|
|
54
|
+
* Poll until at least one event appears for a connection.
|
|
55
|
+
* Returns the events array once non-empty.
|
|
56
|
+
*/
|
|
57
|
+
export declare function pollUntilEvent(manager: IngestorManager, callerAlias: string, connection: string, timeoutMs?: number): Promise<IngestedEvent[]>;
|
|
58
|
+
/** Common shape every IngestedEvent must satisfy (for use with toMatchObject). */
|
|
59
|
+
export declare const INGESTED_EVENT_SHAPE: {
|
|
60
|
+
id: any;
|
|
61
|
+
idempotencyKey: any;
|
|
62
|
+
receivedAt: any;
|
|
63
|
+
receivedAtMs: any;
|
|
64
|
+
callerAlias: string;
|
|
65
|
+
source: any;
|
|
66
|
+
eventType: any;
|
|
67
|
+
data: any;
|
|
68
|
+
};
|
|
69
|
+
//# sourceMappingURL=setup.d.ts.map
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared E2E test setup for connection ingestor tests.
|
|
3
|
+
*
|
|
4
|
+
* Provides helpers to:
|
|
5
|
+
* - Load .env.e2e environment variables
|
|
6
|
+
* - Build a RemoteServerConfig with in-memory keys and requested connections
|
|
7
|
+
* - Boot an Express server on a random port
|
|
8
|
+
* - Generate valid webhook signatures (GitHub, Trello)
|
|
9
|
+
* - Wait for ingestor state transitions and event arrival
|
|
10
|
+
*/
|
|
11
|
+
import crypto from 'node:crypto';
|
|
12
|
+
import path from 'node:path';
|
|
13
|
+
import dotenv from 'dotenv';
|
|
14
|
+
import { expect } from 'vitest';
|
|
15
|
+
import { createApp } from '../../server.js';
|
|
16
|
+
import { generateKeyBundle, extractPublicKeys } from '../../../shared/crypto/index.js';
|
|
17
|
+
import { IngestorManager } from '../manager.js';
|
|
18
|
+
// ── Env loading ─────────────────────────────────────────────────────────
|
|
19
|
+
/** Load .env.e2e from the project root (idempotent). */
|
|
20
|
+
let envLoaded = false;
|
|
21
|
+
export function loadE2EEnv() {
|
|
22
|
+
if (envLoaded)
|
|
23
|
+
return;
|
|
24
|
+
dotenv.config({ path: path.resolve(import.meta.dirname, '../../../../.env.e2e') });
|
|
25
|
+
envLoaded = true;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Check that all required env vars are set.
|
|
29
|
+
* Returns the missing var names (empty array = all present).
|
|
30
|
+
*/
|
|
31
|
+
export function checkEnvVars(vars) {
|
|
32
|
+
loadE2EEnv();
|
|
33
|
+
return vars.filter((v) => !process.env[v]);
|
|
34
|
+
}
|
|
35
|
+
// ── Config builder ──────────────────────────────────────────────────────
|
|
36
|
+
/**
|
|
37
|
+
* Build a RemoteServerConfig for E2E tests.
|
|
38
|
+
*
|
|
39
|
+
* Creates a single caller ("e2e-client") with the requested connections.
|
|
40
|
+
* Env vars are injected into caller.env so secret resolution works against
|
|
41
|
+
* process.env (already loaded by loadE2EEnv).
|
|
42
|
+
*/
|
|
43
|
+
export function buildE2EConfig(connections, opts) {
|
|
44
|
+
return {
|
|
45
|
+
host: '127.0.0.1',
|
|
46
|
+
port: 0,
|
|
47
|
+
callers: {
|
|
48
|
+
'e2e-client': {
|
|
49
|
+
connections,
|
|
50
|
+
env: opts?.env,
|
|
51
|
+
ingestorOverrides: opts?.ingestorOverrides,
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
rateLimitPerMinute: 600,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Boot an Express server with the given config on a random port.
|
|
59
|
+
* Starts all ingestors and returns handles for testing.
|
|
60
|
+
*/
|
|
61
|
+
export async function bootServer(config) {
|
|
62
|
+
const serverKeys = generateKeyBundle();
|
|
63
|
+
const clientKeys = generateKeyBundle();
|
|
64
|
+
const clientPub = extractPublicKeys(clientKeys);
|
|
65
|
+
const ingestorManager = new IngestorManager(config);
|
|
66
|
+
const app = createApp({
|
|
67
|
+
config,
|
|
68
|
+
ownKeys: serverKeys,
|
|
69
|
+
authorizedPeers: [{ alias: 'e2e-client', keys: clientPub }],
|
|
70
|
+
ingestorManager,
|
|
71
|
+
disableRateLimiting: true,
|
|
72
|
+
});
|
|
73
|
+
// Start ingestors
|
|
74
|
+
await ingestorManager.startAll();
|
|
75
|
+
// Listen on random port
|
|
76
|
+
const server = await new Promise((resolve) => {
|
|
77
|
+
const s = app.listen(0, '127.0.0.1', () => resolve(s));
|
|
78
|
+
});
|
|
79
|
+
const addr = server.address();
|
|
80
|
+
const baseUrl = `http://127.0.0.1:${addr.port}`;
|
|
81
|
+
return {
|
|
82
|
+
server,
|
|
83
|
+
baseUrl,
|
|
84
|
+
ingestorManager,
|
|
85
|
+
teardown: async () => {
|
|
86
|
+
await ingestorManager.stopAll();
|
|
87
|
+
await new Promise((resolve, reject) => {
|
|
88
|
+
server.close((err) => (err ? reject(err) : resolve()));
|
|
89
|
+
});
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
// ── Signature helpers ───────────────────────────────────────────────────
|
|
94
|
+
/** Generate a valid GitHub HMAC-SHA256 signature for a raw body. */
|
|
95
|
+
export function signGitHubPayload(rawBody, secret) {
|
|
96
|
+
const buf = typeof rawBody === 'string' ? Buffer.from(rawBody) : rawBody;
|
|
97
|
+
const sig = crypto.createHmac('sha256', secret).update(buf).digest('hex');
|
|
98
|
+
return `sha256=${sig}`;
|
|
99
|
+
}
|
|
100
|
+
/** Generate a valid Trello HMAC-SHA1 base64 signature for body + callbackUrl. */
|
|
101
|
+
export function signTrelloPayload(rawBody, callbackUrl, secret) {
|
|
102
|
+
const bodyStr = typeof rawBody === 'string' ? rawBody : rawBody.toString('utf-8');
|
|
103
|
+
return crypto.createHmac('sha1', secret).update(bodyStr + callbackUrl).digest('base64');
|
|
104
|
+
}
|
|
105
|
+
// ── Polling helpers ─────────────────────────────────────────────────────
|
|
106
|
+
/**
|
|
107
|
+
* Wait for an ingestor to reach a specific state.
|
|
108
|
+
* Polls every 250ms until the timeout is reached.
|
|
109
|
+
*/
|
|
110
|
+
export async function waitForIngestorState(manager, callerAlias, connection, targetState, timeoutMs = 15_000) {
|
|
111
|
+
const start = Date.now();
|
|
112
|
+
while (Date.now() - start < timeoutMs) {
|
|
113
|
+
const statuses = manager.getStatuses(callerAlias);
|
|
114
|
+
const status = statuses.find((s) => s.connection === connection);
|
|
115
|
+
if (status?.state === targetState)
|
|
116
|
+
return;
|
|
117
|
+
await new Promise((r) => setTimeout(r, 250));
|
|
118
|
+
}
|
|
119
|
+
throw new Error(`Timed out waiting for ${connection} to reach state "${targetState}" (${timeoutMs}ms)`);
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Poll until at least one event appears for a connection.
|
|
123
|
+
* Returns the events array once non-empty.
|
|
124
|
+
*/
|
|
125
|
+
export async function pollUntilEvent(manager, callerAlias, connection, timeoutMs = 10_000) {
|
|
126
|
+
const start = Date.now();
|
|
127
|
+
while (Date.now() - start < timeoutMs) {
|
|
128
|
+
const events = manager.getEvents(callerAlias, connection);
|
|
129
|
+
if (events.length > 0)
|
|
130
|
+
return events;
|
|
131
|
+
await new Promise((r) => setTimeout(r, 250));
|
|
132
|
+
}
|
|
133
|
+
throw new Error(`Timed out waiting for events from ${connection} (${timeoutMs}ms)`);
|
|
134
|
+
}
|
|
135
|
+
// ── Shared assertions ───────────────────────────────────────────────────
|
|
136
|
+
/** Common shape every IngestedEvent must satisfy (for use with toMatchObject). */
|
|
137
|
+
export const INGESTED_EVENT_SHAPE = {
|
|
138
|
+
id: expect.any(Number),
|
|
139
|
+
idempotencyKey: expect.any(String),
|
|
140
|
+
receivedAt: expect.stringMatching(/^\d{4}-\d{2}-\d{2}T/),
|
|
141
|
+
receivedAtMs: expect.any(Number),
|
|
142
|
+
callerAlias: 'e2e-client',
|
|
143
|
+
source: expect.any(String),
|
|
144
|
+
eventType: expect.any(String),
|
|
145
|
+
data: expect.anything(),
|
|
146
|
+
};
|
|
147
|
+
//# sourceMappingURL=setup.js.map
|
|
@@ -43,6 +43,10 @@ export declare class IngestorManager {
|
|
|
43
43
|
private readonly config;
|
|
44
44
|
/** Active ingestor instances, keyed by `callerAlias:connectionAlias:instanceId`. */
|
|
45
45
|
private ingestors;
|
|
46
|
+
/** Trigger rule engines per caller. Created during startAll() for callers with triggerRules. */
|
|
47
|
+
private triggerEngines;
|
|
48
|
+
/** Global event listeners (e.g. SSE streams). Called for every event from every ingestor. */
|
|
49
|
+
private eventListeners;
|
|
46
50
|
/**
|
|
47
51
|
* Optional config loader for hot-reload support. When provided, `startOne()`
|
|
48
52
|
* uses it to get fresh config from disk instead of the constructor snapshot.
|
|
@@ -63,6 +67,12 @@ export declare class IngestorManager {
|
|
|
63
67
|
* Internal: create, register, and start a single ingestor instance.
|
|
64
68
|
*/
|
|
65
69
|
private startIngestor;
|
|
70
|
+
/**
|
|
71
|
+
* Initialize trigger rule engines for callers with triggerRules config.
|
|
72
|
+
* Subscribes to 'event' emissions from matching ingestors and dispatches
|
|
73
|
+
* to Claude Code remote triggers.
|
|
74
|
+
*/
|
|
75
|
+
private initTriggerEngines;
|
|
66
76
|
/**
|
|
67
77
|
* Stop all running ingestors. Called during graceful shutdown.
|
|
68
78
|
*/
|
|
@@ -82,6 +92,15 @@ export declare class IngestorManager {
|
|
|
82
92
|
* Get status of all ingestors for a caller.
|
|
83
93
|
*/
|
|
84
94
|
getStatuses(callerAlias: string): IngestorStatus[];
|
|
95
|
+
/**
|
|
96
|
+
* Subscribe to all events from all ingestors (current and future).
|
|
97
|
+
* Used by the SSE /events/stream endpoint to fan out events to CLI watchers.
|
|
98
|
+
*/
|
|
99
|
+
onEvent(listener: (event: IngestedEvent) => void): void;
|
|
100
|
+
/** Unsubscribe a global event listener. */
|
|
101
|
+
offEvent(listener: (event: IngestedEvent) => void): void;
|
|
102
|
+
/** Forward an ingestor event to all global listeners. */
|
|
103
|
+
private notifyEventListeners;
|
|
85
104
|
/**
|
|
86
105
|
* Find all webhook ingestor instances that match a given webhook path.
|
|
87
106
|
* Returns all matching instances across all callers (for fan-out dispatch).
|