@mcnekoneko/hookstream-cli 0.1.0 → 0.1.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/package.json +2 -2
- package/dist/api.d.ts +0 -52
- package/dist/api.js +0 -212
- package/dist/config.d.ts +0 -9
- package/dist/config.js +0 -45
- package/dist/index.d.ts +0 -2
- package/dist/index.js +0 -225
- package/dist/types.d.ts +0 -23
- package/dist/types.js +0 -1
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mcnekoneko/hookstream-cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "CLI for managing hookstream channels",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
-
"hookstream": "
|
|
7
|
+
"hookstream": "dist/index.js"
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
10
|
"dist",
|
package/dist/api.d.ts
DELETED
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
import type { ChannelConfig, RelayEvent, SignatureAlgorithm } from "./types.js";
|
|
2
|
-
export type CreateChannelInput = {
|
|
3
|
-
id: string;
|
|
4
|
-
token?: string;
|
|
5
|
-
eventHeader?: string;
|
|
6
|
-
maxHistory?: number;
|
|
7
|
-
signature?: {
|
|
8
|
-
header: string;
|
|
9
|
-
algorithm: SignatureAlgorithm;
|
|
10
|
-
secret: string;
|
|
11
|
-
prefix?: string;
|
|
12
|
-
};
|
|
13
|
-
};
|
|
14
|
-
export type TestResult = {
|
|
15
|
-
ok: boolean;
|
|
16
|
-
channelId: string;
|
|
17
|
-
webhookStatus: number;
|
|
18
|
-
eventReceived: boolean;
|
|
19
|
-
eventId?: string;
|
|
20
|
-
roundTripMs?: number;
|
|
21
|
-
error?: string;
|
|
22
|
-
};
|
|
23
|
-
export declare class HookstreamClient {
|
|
24
|
-
private readonly url;
|
|
25
|
-
private readonly adminKey;
|
|
26
|
-
constructor(url: string, adminKey: string);
|
|
27
|
-
private headers;
|
|
28
|
-
listChannels(): Promise<ChannelConfig[]>;
|
|
29
|
-
createChannel(input: CreateChannelInput): Promise<ChannelConfig>;
|
|
30
|
-
deleteChannel(id: string): Promise<{
|
|
31
|
-
deleted: string;
|
|
32
|
-
}>;
|
|
33
|
-
/**
|
|
34
|
-
* End-to-end test: subscribe to SSE, send a test webhook, verify delivery.
|
|
35
|
-
*/
|
|
36
|
-
testChannel(channelId: string, opts?: {
|
|
37
|
-
token?: string;
|
|
38
|
-
timeoutMs?: number;
|
|
39
|
-
}): Promise<TestResult>;
|
|
40
|
-
/**
|
|
41
|
-
* Subscribe to a channel's SSE stream. Calls `onEvent` for each received
|
|
42
|
-
* event. Returns when the stream closes or `signal` is aborted.
|
|
43
|
-
*/
|
|
44
|
-
subscribe(channelId: string, opts: {
|
|
45
|
-
token?: string;
|
|
46
|
-
lastEventId?: string;
|
|
47
|
-
onEvent: (event: RelayEvent) => void;
|
|
48
|
-
onKeepalive?: () => void;
|
|
49
|
-
onError?: (error: Error) => void;
|
|
50
|
-
signal?: AbortSignal;
|
|
51
|
-
}): Promise<void>;
|
|
52
|
-
}
|
package/dist/api.js
DELETED
|
@@ -1,212 +0,0 @@
|
|
|
1
|
-
export class HookstreamClient {
|
|
2
|
-
url;
|
|
3
|
-
adminKey;
|
|
4
|
-
constructor(url, adminKey) {
|
|
5
|
-
this.url = url;
|
|
6
|
-
this.adminKey = adminKey;
|
|
7
|
-
}
|
|
8
|
-
headers() {
|
|
9
|
-
return {
|
|
10
|
-
Authorization: `Bearer ${this.adminKey}`,
|
|
11
|
-
"Content-Type": "application/json",
|
|
12
|
-
};
|
|
13
|
-
}
|
|
14
|
-
async listChannels() {
|
|
15
|
-
const res = await fetch(`${this.url}/admin/channels`, {
|
|
16
|
-
headers: this.headers(),
|
|
17
|
-
});
|
|
18
|
-
const data = (await res.json());
|
|
19
|
-
if (!res.ok)
|
|
20
|
-
throw new Error(data.error);
|
|
21
|
-
return data;
|
|
22
|
-
}
|
|
23
|
-
async createChannel(input) {
|
|
24
|
-
const res = await fetch(`${this.url}/admin/channels`, {
|
|
25
|
-
method: "POST",
|
|
26
|
-
headers: this.headers(),
|
|
27
|
-
body: JSON.stringify(input),
|
|
28
|
-
});
|
|
29
|
-
const data = (await res.json());
|
|
30
|
-
if (!res.ok)
|
|
31
|
-
throw new Error(data.error);
|
|
32
|
-
return data;
|
|
33
|
-
}
|
|
34
|
-
async deleteChannel(id) {
|
|
35
|
-
const res = await fetch(`${this.url}/admin/channels/${id}`, {
|
|
36
|
-
method: "DELETE",
|
|
37
|
-
headers: this.headers(),
|
|
38
|
-
});
|
|
39
|
-
const data = (await res.json());
|
|
40
|
-
if (!res.ok)
|
|
41
|
-
throw new Error(data.error);
|
|
42
|
-
return data;
|
|
43
|
-
}
|
|
44
|
-
/**
|
|
45
|
-
* End-to-end test: subscribe to SSE, send a test webhook, verify delivery.
|
|
46
|
-
*/
|
|
47
|
-
async testChannel(channelId, opts = {}) {
|
|
48
|
-
const timeoutMs = opts.timeoutMs ?? 10_000;
|
|
49
|
-
const testPayload = {
|
|
50
|
-
_hookstream_test: true,
|
|
51
|
-
ts: Date.now(),
|
|
52
|
-
nonce: crypto.randomUUID(),
|
|
53
|
-
};
|
|
54
|
-
const sseUrl = `${this.url}/${channelId}/events`;
|
|
55
|
-
const webhookUrl = `${this.url}/${channelId}`;
|
|
56
|
-
// 1. Connect to SSE
|
|
57
|
-
const sseHeaders = {};
|
|
58
|
-
if (opts.token)
|
|
59
|
-
sseHeaders.Authorization = `Bearer ${opts.token}`;
|
|
60
|
-
const sseRes = await fetch(sseUrl, { headers: sseHeaders });
|
|
61
|
-
if (!sseRes.ok) {
|
|
62
|
-
return {
|
|
63
|
-
ok: false,
|
|
64
|
-
channelId,
|
|
65
|
-
webhookStatus: 0,
|
|
66
|
-
eventReceived: false,
|
|
67
|
-
error: `SSE connect failed: HTTP ${sseRes.status}`,
|
|
68
|
-
};
|
|
69
|
-
}
|
|
70
|
-
const reader = sseRes.body?.getReader();
|
|
71
|
-
if (!reader) {
|
|
72
|
-
return {
|
|
73
|
-
ok: false,
|
|
74
|
-
channelId,
|
|
75
|
-
webhookStatus: 0,
|
|
76
|
-
eventReceived: false,
|
|
77
|
-
error: "SSE response has no readable body",
|
|
78
|
-
};
|
|
79
|
-
}
|
|
80
|
-
const decoder = new TextDecoder();
|
|
81
|
-
const startMs = Date.now();
|
|
82
|
-
// 2. Send test webhook
|
|
83
|
-
const webhookRes = await fetch(webhookUrl, {
|
|
84
|
-
method: "POST",
|
|
85
|
-
headers: { "Content-Type": "application/json" },
|
|
86
|
-
body: JSON.stringify(testPayload),
|
|
87
|
-
});
|
|
88
|
-
if (!webhookRes.ok) {
|
|
89
|
-
reader.cancel().catch(() => { });
|
|
90
|
-
const body = await webhookRes.text().catch(() => "");
|
|
91
|
-
return {
|
|
92
|
-
ok: false,
|
|
93
|
-
channelId,
|
|
94
|
-
webhookStatus: webhookRes.status,
|
|
95
|
-
eventReceived: false,
|
|
96
|
-
error: `Webhook POST failed: HTTP ${webhookRes.status} ${body}`,
|
|
97
|
-
};
|
|
98
|
-
}
|
|
99
|
-
const webhookData = (await webhookRes.json());
|
|
100
|
-
// 3. Read SSE stream until we see our test event or timeout
|
|
101
|
-
let buffer = "";
|
|
102
|
-
const deadline = Date.now() + timeoutMs;
|
|
103
|
-
while (Date.now() < deadline) {
|
|
104
|
-
const remaining = deadline - Date.now();
|
|
105
|
-
const readPromise = reader.read();
|
|
106
|
-
const timeoutPromise = new Promise((resolve) => setTimeout(() => resolve({ done: true, value: undefined }), remaining));
|
|
107
|
-
const { done, value } = await Promise.race([readPromise, timeoutPromise]);
|
|
108
|
-
if (done && !value)
|
|
109
|
-
break;
|
|
110
|
-
if (value)
|
|
111
|
-
buffer += decoder.decode(value, { stream: true });
|
|
112
|
-
// Parse SSE frames from buffer
|
|
113
|
-
const frames = buffer.split("\n\n");
|
|
114
|
-
buffer = frames.pop() ?? "";
|
|
115
|
-
for (const frame of frames) {
|
|
116
|
-
const dataLine = frame.split("\n").find((l) => l.startsWith("data: "));
|
|
117
|
-
if (!dataLine)
|
|
118
|
-
continue;
|
|
119
|
-
try {
|
|
120
|
-
const event = JSON.parse(dataLine.slice(6));
|
|
121
|
-
if (event.id === webhookData.id) {
|
|
122
|
-
const roundTripMs = Date.now() - startMs;
|
|
123
|
-
reader.cancel().catch(() => { });
|
|
124
|
-
return {
|
|
125
|
-
ok: true,
|
|
126
|
-
channelId,
|
|
127
|
-
webhookStatus: webhookRes.status,
|
|
128
|
-
eventReceived: true,
|
|
129
|
-
eventId: event.id,
|
|
130
|
-
roundTripMs,
|
|
131
|
-
};
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
catch {
|
|
135
|
-
// not our event, continue
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
reader.cancel().catch(() => { });
|
|
140
|
-
return {
|
|
141
|
-
ok: false,
|
|
142
|
-
channelId,
|
|
143
|
-
webhookStatus: webhookRes.status,
|
|
144
|
-
eventReceived: false,
|
|
145
|
-
error: `Timeout: event not received within ${timeoutMs}ms`,
|
|
146
|
-
};
|
|
147
|
-
}
|
|
148
|
-
/**
|
|
149
|
-
* Subscribe to a channel's SSE stream. Calls `onEvent` for each received
|
|
150
|
-
* event. Returns when the stream closes or `signal` is aborted.
|
|
151
|
-
*/
|
|
152
|
-
async subscribe(channelId, opts) {
|
|
153
|
-
const sseUrl = `${this.url}/${channelId}/events`;
|
|
154
|
-
const headers = {};
|
|
155
|
-
if (opts.token)
|
|
156
|
-
headers.Authorization = `Bearer ${opts.token}`;
|
|
157
|
-
if (opts.lastEventId)
|
|
158
|
-
headers["Last-Event-ID"] = opts.lastEventId;
|
|
159
|
-
const res = await fetch(sseUrl, { headers, signal: opts.signal });
|
|
160
|
-
if (!res.ok) {
|
|
161
|
-
const body = await res.text().catch(() => "");
|
|
162
|
-
throw new Error(`SSE connect failed: HTTP ${res.status} ${body}`);
|
|
163
|
-
}
|
|
164
|
-
const reader = res.body?.getReader();
|
|
165
|
-
if (!reader)
|
|
166
|
-
throw new Error("SSE response has no readable body");
|
|
167
|
-
const decoder = new TextDecoder();
|
|
168
|
-
let buffer = "";
|
|
169
|
-
try {
|
|
170
|
-
while (true) {
|
|
171
|
-
if (opts.signal?.aborted)
|
|
172
|
-
break;
|
|
173
|
-
const { done, value } = await reader.read();
|
|
174
|
-
if (done)
|
|
175
|
-
break;
|
|
176
|
-
buffer += decoder.decode(value, { stream: true });
|
|
177
|
-
const frames = buffer.split("\n\n");
|
|
178
|
-
buffer = frames.pop() ?? "";
|
|
179
|
-
for (const frame of frames) {
|
|
180
|
-
// Keepalive
|
|
181
|
-
if (frame.trim() === ":keepalive") {
|
|
182
|
-
opts.onKeepalive?.();
|
|
183
|
-
continue;
|
|
184
|
-
}
|
|
185
|
-
const dataLine = frame
|
|
186
|
-
.split("\n")
|
|
187
|
-
.find((l) => l.startsWith("data: "));
|
|
188
|
-
if (!dataLine)
|
|
189
|
-
continue;
|
|
190
|
-
try {
|
|
191
|
-
const event = JSON.parse(dataLine.slice(6));
|
|
192
|
-
opts.onEvent(event);
|
|
193
|
-
}
|
|
194
|
-
catch {
|
|
195
|
-
// skip malformed frames
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
catch (err) {
|
|
201
|
-
if (opts.signal?.aborted)
|
|
202
|
-
return;
|
|
203
|
-
if (opts.onError)
|
|
204
|
-
opts.onError(err);
|
|
205
|
-
else
|
|
206
|
-
throw err;
|
|
207
|
-
}
|
|
208
|
-
finally {
|
|
209
|
-
reader.cancel().catch(() => { });
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
}
|
package/dist/config.d.ts
DELETED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
export type Profile = {
|
|
2
|
-
url: string;
|
|
3
|
-
adminKey: string;
|
|
4
|
-
};
|
|
5
|
-
export type Config = Record<string, Profile>;
|
|
6
|
-
export declare function loadConfig(): Config;
|
|
7
|
-
export declare function saveConfig(config: Config): void;
|
|
8
|
-
export declare function getProfile(name?: string): Profile | undefined;
|
|
9
|
-
export declare function runConfigure(profileName?: string): Promise<void>;
|
package/dist/config.js
DELETED
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
-
import { homedir } from "node:os";
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
-
import { createInterface } from "node:readline/promises";
|
|
5
|
-
const CONFIG_DIR = join(homedir(), ".config", "hookstream-cli");
|
|
6
|
-
const CONFIG_FILE = join(CONFIG_DIR, "config.json");
|
|
7
|
-
export function loadConfig() {
|
|
8
|
-
if (!existsSync(CONFIG_FILE))
|
|
9
|
-
return {};
|
|
10
|
-
try {
|
|
11
|
-
return JSON.parse(readFileSync(CONFIG_FILE, "utf-8"));
|
|
12
|
-
}
|
|
13
|
-
catch {
|
|
14
|
-
return {};
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
export function saveConfig(config) {
|
|
18
|
-
if (!existsSync(CONFIG_DIR))
|
|
19
|
-
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
20
|
-
writeFileSync(CONFIG_FILE, `${JSON.stringify(config, null, 2)}\n`, "utf-8");
|
|
21
|
-
}
|
|
22
|
-
export function getProfile(name = "default") {
|
|
23
|
-
return loadConfig()[name];
|
|
24
|
-
}
|
|
25
|
-
export async function runConfigure(profileName = "default") {
|
|
26
|
-
const config = loadConfig();
|
|
27
|
-
const existing = config[profileName];
|
|
28
|
-
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
29
|
-
console.log(profileName === "default"
|
|
30
|
-
? "Configure hookstream CLI (default profile)"
|
|
31
|
-
: `Configure hookstream CLI (profile: ${profileName})`);
|
|
32
|
-
const url = await rl.question(`Worker URL${existing?.url ? ` [${existing.url}]` : ""}: `);
|
|
33
|
-
const adminKey = await rl.question(`Admin key${existing?.adminKey ? " [****]" : ""}: `);
|
|
34
|
-
rl.close();
|
|
35
|
-
config[profileName] = {
|
|
36
|
-
url: url.trim() || existing?.url || "",
|
|
37
|
-
adminKey: adminKey.trim() || existing?.adminKey || "",
|
|
38
|
-
};
|
|
39
|
-
if (!config[profileName].url || !config[profileName].adminKey) {
|
|
40
|
-
console.error("Error: URL and admin key are required.");
|
|
41
|
-
process.exit(1);
|
|
42
|
-
}
|
|
43
|
-
saveConfig(config);
|
|
44
|
-
console.log(`\nSaved profile '${profileName}' to ${CONFIG_FILE}`);
|
|
45
|
-
}
|
package/dist/index.d.ts
DELETED
package/dist/index.js
DELETED
|
@@ -1,225 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import { Command, InvalidArgumentError } from "commander";
|
|
3
|
-
import { HookstreamClient } from "./api.js";
|
|
4
|
-
import { getProfile, runConfigure } from "./config.js";
|
|
5
|
-
const program = new Command();
|
|
6
|
-
program
|
|
7
|
-
.name("hookstream")
|
|
8
|
-
.description("CLI for managing hookstream channels")
|
|
9
|
-
.option("-p, --profile <name>", "Config profile to use", "default")
|
|
10
|
-
.option("-u, --url <url>", "Worker URL (overrides profile)")
|
|
11
|
-
.option("-k, --admin-key <key>", "Admin key (overrides profile)");
|
|
12
|
-
function getClient() {
|
|
13
|
-
const opts = program.opts();
|
|
14
|
-
// Priority: flag > env var > profile
|
|
15
|
-
const url = opts.url ?? process.env.HOOKSTREAM_URL ?? getProfile(opts.profile)?.url;
|
|
16
|
-
const adminKey = opts.adminKey ??
|
|
17
|
-
process.env.HOOKSTREAM_ADMIN_KEY ??
|
|
18
|
-
getProfile(opts.profile)?.adminKey;
|
|
19
|
-
if (!url) {
|
|
20
|
-
console.error(`Error: Worker URL is required.\n Run: hookstream configure --profile ${opts.profile}`);
|
|
21
|
-
process.exit(1);
|
|
22
|
-
}
|
|
23
|
-
if (!adminKey) {
|
|
24
|
-
console.error(`Error: Admin key is required.\n Run: hookstream configure --profile ${opts.profile}`);
|
|
25
|
-
process.exit(1);
|
|
26
|
-
}
|
|
27
|
-
return new HookstreamClient(url.replace(/\/$/, ""), adminKey);
|
|
28
|
-
}
|
|
29
|
-
// ─── configure ───────────────────────────────────────────────────────────────
|
|
30
|
-
program
|
|
31
|
-
.command("configure")
|
|
32
|
-
.description("Save Worker URL and admin key to a config profile")
|
|
33
|
-
.action(async () => {
|
|
34
|
-
const profileName = program.opts().profile;
|
|
35
|
-
await runConfigure(profileName);
|
|
36
|
-
});
|
|
37
|
-
// ─── channels ────────────────────────────────────────────────────────────────
|
|
38
|
-
const channels = program.command("channels").description("Manage channels");
|
|
39
|
-
channels
|
|
40
|
-
.command("list")
|
|
41
|
-
.description("List all channels")
|
|
42
|
-
.action(async () => {
|
|
43
|
-
try {
|
|
44
|
-
const client = getClient();
|
|
45
|
-
const list = await client.listChannels();
|
|
46
|
-
if (list.length === 0) {
|
|
47
|
-
console.log("No channels found.");
|
|
48
|
-
return;
|
|
49
|
-
}
|
|
50
|
-
for (const ch of list) {
|
|
51
|
-
const sig = ch.signature
|
|
52
|
-
? ` sig:${ch.signature.algorithm}(${ch.signature.header})`
|
|
53
|
-
: "";
|
|
54
|
-
const tok = ch.token ? " token:✓" : "";
|
|
55
|
-
const ev = ch.eventHeader ? ` event:${ch.eventHeader}` : "";
|
|
56
|
-
console.log(` ${ch.id.padEnd(24)} maxHistory:${ch.maxHistory}${sig}${tok}${ev} [${ch.createdAt}]`);
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
catch (err) {
|
|
60
|
-
console.error(`Error: ${err.message}`);
|
|
61
|
-
process.exit(1);
|
|
62
|
-
}
|
|
63
|
-
});
|
|
64
|
-
channels
|
|
65
|
-
.command("create")
|
|
66
|
-
.description("Create a channel")
|
|
67
|
-
.requiredOption("--id <id>", "Channel ID (a-z0-9_-, max 64)")
|
|
68
|
-
.option("--token <token>", "Bearer token for SSE access")
|
|
69
|
-
.option("--event-header <header>", "Header to read event type from")
|
|
70
|
-
.option("--max-history <n>", "Ring buffer size for reconnect replay", (v) => {
|
|
71
|
-
const n = parseInt(v, 10);
|
|
72
|
-
if (Number.isNaN(n) || n < 1)
|
|
73
|
-
throw new InvalidArgumentError("Must be a positive integer.");
|
|
74
|
-
return n;
|
|
75
|
-
}, 50)
|
|
76
|
-
.option("--sig-header <header>", "Signature header name")
|
|
77
|
-
.option("--sig-algorithm <alg>", "Signature algorithm (hmac-sha256-hex|hmac-sha256-base64)")
|
|
78
|
-
.option("--sig-secret <secret>", "HMAC secret")
|
|
79
|
-
.option("--sig-prefix <prefix>", "Prefix to strip before comparing (e.g. sha256=)")
|
|
80
|
-
.action(async (opts) => {
|
|
81
|
-
try {
|
|
82
|
-
const client = getClient();
|
|
83
|
-
// Build signature config if provided
|
|
84
|
-
let signature;
|
|
85
|
-
if (opts.sigHeader || opts.sigAlgorithm || opts.sigSecret) {
|
|
86
|
-
if (!opts.sigHeader || !opts.sigAlgorithm || !opts.sigSecret) {
|
|
87
|
-
console.error("Error: --sig-header, --sig-algorithm, and --sig-secret are all required when configuring signature verification.");
|
|
88
|
-
process.exit(1);
|
|
89
|
-
}
|
|
90
|
-
if (opts.sigAlgorithm !== "hmac-sha256-hex" &&
|
|
91
|
-
opts.sigAlgorithm !== "hmac-sha256-base64") {
|
|
92
|
-
console.error("Error: --sig-algorithm must be 'hmac-sha256-hex' or 'hmac-sha256-base64'.");
|
|
93
|
-
process.exit(1);
|
|
94
|
-
}
|
|
95
|
-
signature = {
|
|
96
|
-
header: opts.sigHeader,
|
|
97
|
-
algorithm: opts.sigAlgorithm,
|
|
98
|
-
secret: opts.sigSecret,
|
|
99
|
-
prefix: opts.sigPrefix,
|
|
100
|
-
};
|
|
101
|
-
}
|
|
102
|
-
const ch = await client.createChannel({
|
|
103
|
-
id: opts.id,
|
|
104
|
-
token: opts.token,
|
|
105
|
-
eventHeader: opts.eventHeader,
|
|
106
|
-
maxHistory: opts.maxHistory,
|
|
107
|
-
signature,
|
|
108
|
-
});
|
|
109
|
-
console.log(`Channel created: ${ch.id}`);
|
|
110
|
-
console.log(JSON.stringify(ch, null, 2));
|
|
111
|
-
}
|
|
112
|
-
catch (err) {
|
|
113
|
-
console.error(`Error: ${err.message}`);
|
|
114
|
-
process.exit(1);
|
|
115
|
-
}
|
|
116
|
-
});
|
|
117
|
-
channels
|
|
118
|
-
.command("delete <id>")
|
|
119
|
-
.description("Delete a channel")
|
|
120
|
-
.action(async (id) => {
|
|
121
|
-
try {
|
|
122
|
-
const client = getClient();
|
|
123
|
-
const result = await client.deleteChannel(id);
|
|
124
|
-
console.log(`Deleted: ${result.deleted}`);
|
|
125
|
-
}
|
|
126
|
-
catch (err) {
|
|
127
|
-
console.error(`Error: ${err.message}`);
|
|
128
|
-
process.exit(1);
|
|
129
|
-
}
|
|
130
|
-
});
|
|
131
|
-
channels
|
|
132
|
-
.command("subscribe <id>")
|
|
133
|
-
.description("Subscribe to a channel's SSE stream and print events in real-time")
|
|
134
|
-
.option("--token <token>", "Bearer token for SSE (if channel requires auth)")
|
|
135
|
-
.option("--last-event-id <id>", "Resume from a specific event ID")
|
|
136
|
-
.option("--json", "Output raw JSON per event (one line per event)")
|
|
137
|
-
.action(async (id, opts) => {
|
|
138
|
-
const client = getClient();
|
|
139
|
-
const baseUrl = program.opts().url ?? getProfile(program.opts().profile)?.url ?? "";
|
|
140
|
-
if (!opts.json) {
|
|
141
|
-
console.log(`Subscribing to: ${baseUrl}/${id}/events`);
|
|
142
|
-
if (opts.lastEventId)
|
|
143
|
-
console.log(` Last-Event-ID: ${opts.lastEventId}`);
|
|
144
|
-
console.log(" Press Ctrl+C to stop\n");
|
|
145
|
-
}
|
|
146
|
-
const ac = new AbortController();
|
|
147
|
-
process.on("SIGINT", () => ac.abort());
|
|
148
|
-
process.on("SIGTERM", () => ac.abort());
|
|
149
|
-
try {
|
|
150
|
-
await client.subscribe(id, {
|
|
151
|
-
token: opts.token,
|
|
152
|
-
lastEventId: opts.lastEventId,
|
|
153
|
-
signal: ac.signal,
|
|
154
|
-
onEvent: (event) => {
|
|
155
|
-
if (opts.json) {
|
|
156
|
-
console.log(JSON.stringify(event));
|
|
157
|
-
}
|
|
158
|
-
else {
|
|
159
|
-
const time = new Date(event.timestamp).toLocaleTimeString();
|
|
160
|
-
console.log(`[${time}] event=${event.event} id=${event.id}`);
|
|
161
|
-
console.log(` ${JSON.stringify(event.payload, null, 2).split("\n").join("\n ")}`);
|
|
162
|
-
console.log();
|
|
163
|
-
}
|
|
164
|
-
},
|
|
165
|
-
onKeepalive: () => {
|
|
166
|
-
if (!opts.json) {
|
|
167
|
-
process.stdout.write(".");
|
|
168
|
-
}
|
|
169
|
-
},
|
|
170
|
-
});
|
|
171
|
-
if (!opts.json)
|
|
172
|
-
console.log("\nStream closed.");
|
|
173
|
-
}
|
|
174
|
-
catch (err) {
|
|
175
|
-
if (ac.signal.aborted) {
|
|
176
|
-
if (!opts.json)
|
|
177
|
-
console.log("\nDisconnected.");
|
|
178
|
-
}
|
|
179
|
-
else {
|
|
180
|
-
console.error(`Error: ${err.message}`);
|
|
181
|
-
process.exit(1);
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
});
|
|
185
|
-
channels
|
|
186
|
-
.command("test <id>")
|
|
187
|
-
.description("End-to-end test: subscribe SSE → send test webhook → verify delivery")
|
|
188
|
-
.option("--token <token>", "Bearer token for SSE (if channel requires auth)")
|
|
189
|
-
.option("--timeout <ms>", "Timeout in milliseconds", (v) => {
|
|
190
|
-
const n = parseInt(v, 10);
|
|
191
|
-
if (Number.isNaN(n) || n < 1)
|
|
192
|
-
throw new InvalidArgumentError("Must be a positive integer.");
|
|
193
|
-
return n;
|
|
194
|
-
}, 10_000)
|
|
195
|
-
.action(async (id, opts) => {
|
|
196
|
-
try {
|
|
197
|
-
const client = getClient();
|
|
198
|
-
console.log(`Testing channel: ${id}`);
|
|
199
|
-
console.log(` SSE: ${program.opts().url ?? getProfile(program.opts().profile)?.url}/${id}/events`);
|
|
200
|
-
console.log(` Webhook: ${program.opts().url ?? getProfile(program.opts().profile)?.url}/${id}`);
|
|
201
|
-
console.log();
|
|
202
|
-
const result = await client.testChannel(id, {
|
|
203
|
-
token: opts.token,
|
|
204
|
-
timeoutMs: opts.timeout,
|
|
205
|
-
});
|
|
206
|
-
if (result.ok) {
|
|
207
|
-
console.log(`✅ PASS`);
|
|
208
|
-
console.log(` Webhook POST: HTTP ${result.webhookStatus}`);
|
|
209
|
-
console.log(` SSE received: ${result.eventId}`);
|
|
210
|
-
console.log(` Round-trip: ${result.roundTripMs}ms`);
|
|
211
|
-
}
|
|
212
|
-
else {
|
|
213
|
-
console.log(`❌ FAIL`);
|
|
214
|
-
if (result.webhookStatus)
|
|
215
|
-
console.log(` Webhook POST: HTTP ${result.webhookStatus}`);
|
|
216
|
-
console.log(` Error: ${result.error}`);
|
|
217
|
-
process.exit(1);
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
catch (err) {
|
|
221
|
-
console.error(`Error: ${err.message}`);
|
|
222
|
-
process.exit(1);
|
|
223
|
-
}
|
|
224
|
-
});
|
|
225
|
-
program.parse();
|
package/dist/types.d.ts
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
export type SignatureAlgorithm = "hmac-sha256-hex" | "hmac-sha256-base64";
|
|
2
|
-
export type SignatureConfig = {
|
|
3
|
-
header: string;
|
|
4
|
-
algorithm: SignatureAlgorithm;
|
|
5
|
-
prefix?: string;
|
|
6
|
-
secret: string;
|
|
7
|
-
};
|
|
8
|
-
export type ChannelConfig = {
|
|
9
|
-
id: string;
|
|
10
|
-
signature?: Omit<SignatureConfig, "secret">;
|
|
11
|
-
token?: string;
|
|
12
|
-
eventHeader?: string;
|
|
13
|
-
maxHistory: number;
|
|
14
|
-
createdAt: string;
|
|
15
|
-
};
|
|
16
|
-
export type RelayEvent = {
|
|
17
|
-
id: string;
|
|
18
|
-
channel: string;
|
|
19
|
-
event: string;
|
|
20
|
-
timestamp: string;
|
|
21
|
-
source?: string;
|
|
22
|
-
payload: unknown;
|
|
23
|
-
};
|
package/dist/types.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|