hivenet 0.4.1 → 0.6.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.
@@ -0,0 +1,210 @@
1
+ import {
2
+ FeedbackError,
3
+ PROTOCOL_VERSION,
4
+ WELL_KNOWN_PATH,
5
+ __toESM,
6
+ generateThreadId,
7
+ parseDsn,
8
+ postEvent,
9
+ registryUrl,
10
+ require_arg,
11
+ validateDescriptor,
12
+ validateFeedbackEvent
13
+ } from "./chunk-DNR6HNJD.js";
14
+
15
+ // src/doctor.ts
16
+ var import_arg = __toESM(require_arg(), 1);
17
+ var FETCH_TIMEOUT_MS = 1e4;
18
+ async function runDoctor(argv, cliVersion) {
19
+ const args = (0, import_arg.default)(
20
+ {
21
+ "--help": Boolean,
22
+ "--to": String,
23
+ "--dsn": String,
24
+ "--smoke": Boolean,
25
+ "--json": Boolean,
26
+ "-h": "--help",
27
+ "-t": "--to"
28
+ },
29
+ { argv, permissive: false }
30
+ );
31
+ if (args["--help"] || !args["--to"] && !args["--dsn"]) {
32
+ printDoctorHelp();
33
+ return;
34
+ }
35
+ const checks = [];
36
+ let endpoint;
37
+ let writeKey;
38
+ let slug;
39
+ let descriptor;
40
+ if (args["--dsn"]) {
41
+ try {
42
+ const destination = parseDsn(args["--dsn"]);
43
+ endpoint = destination.endpoint;
44
+ writeKey = destination.writeKey;
45
+ slug = destination.to;
46
+ checks.push({ name: "dsn", ok: true, detail: `endpoint ${endpoint}` });
47
+ } catch (error) {
48
+ checks.push({ name: "dsn", ok: false, detail: messageOf(error) });
49
+ }
50
+ } else {
51
+ const to = args["--to"].trim();
52
+ const descriptorUrl = to.includes(".") ? `https://${to}${WELL_KNOWN_PATH}` : `${registryUrl()}/v1/registry/${to}`;
53
+ const result = await checkDescriptor(descriptorUrl);
54
+ checks.push(result.check);
55
+ descriptor = result.descriptor;
56
+ endpoint = descriptor?.endpoint;
57
+ writeKey = descriptor?.writeKey;
58
+ slug = descriptor?.slug ?? (to.includes(".") ? void 0 : to);
59
+ }
60
+ if (endpoint) {
61
+ checks.push(checkEndpointUrl(endpoint));
62
+ checks.push(
63
+ writeKey ? {
64
+ name: "write key",
65
+ ok: true,
66
+ detail: `${writeKey.slice(0, 12)}\u2026 (write-only, safe to publish)`
67
+ } : {
68
+ name: "write key",
69
+ ok: true,
70
+ detail: "none published \u2014 the ingest must accept keyless submissions for this project"
71
+ }
72
+ );
73
+ const categories = descriptor?.categories ? Object.keys(descriptor.categories) : [];
74
+ checks.push({
75
+ name: "categories",
76
+ ok: true,
77
+ detail: categories.length > 0 ? categories.join(", ") : "none published \u2014 agents will invent free-text subjects"
78
+ });
79
+ if (args["--smoke"]) {
80
+ checks.push(await sendSmokeEvent(endpoint, writeKey, slug, cliVersion));
81
+ }
82
+ }
83
+ const healthy = checks.every((check) => check.ok);
84
+ if (args["--json"]) {
85
+ console.log(JSON.stringify({ ok: healthy, checks }, null, 2));
86
+ } else {
87
+ for (const check of checks) {
88
+ console.log(`${check.ok ? "\u2713" : "\u2717"} ${check.name}: ${check.detail}`);
89
+ }
90
+ if (healthy && !args["--smoke"]) {
91
+ console.log(" Add --smoke to send one disposable event through the real pipe.");
92
+ }
93
+ }
94
+ if (!healthy) {
95
+ process.exitCode = 1;
96
+ }
97
+ }
98
+ async function checkDescriptor(url) {
99
+ const name = url.includes(WELL_KNOWN_PATH) ? "well-known descriptor" : "registry entry";
100
+ let response;
101
+ try {
102
+ response = await fetch(url, {
103
+ signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
104
+ headers: { accept: "application/json" }
105
+ });
106
+ } catch (error) {
107
+ return { check: { name, ok: false, detail: `could not reach ${url} (${messageOf(error)})` } };
108
+ }
109
+ if (!response.ok) {
110
+ return { check: { name, ok: false, detail: `${url} responded ${response.status}` } };
111
+ }
112
+ const contentType = response.headers.get("content-type") ?? "unknown";
113
+ const body = await response.text();
114
+ let parsed;
115
+ try {
116
+ parsed = JSON.parse(body);
117
+ } catch {
118
+ const spaHint = /<[a-z!]/i.test(body.trimStart()) ? " \u2014 looks like an HTML page (an SPA catch-all route?); serve the JSON file at exactly this path" : "";
119
+ return {
120
+ check: { name, ok: false, detail: `${url} returned ${contentType}, not JSON${spaHint}` }
121
+ };
122
+ }
123
+ const validated = validateDescriptor(parsed);
124
+ if (!validated.ok) {
125
+ return {
126
+ check: {
127
+ name,
128
+ ok: false,
129
+ detail: `${url} is not a valid descriptor: ${validated.errors.join("; ")}`
130
+ }
131
+ };
132
+ }
133
+ return {
134
+ check: { name, ok: true, detail: `${url} (v${validated.data.v}, ${validated.data.name})` },
135
+ descriptor: validated.data
136
+ };
137
+ }
138
+ function checkEndpointUrl(endpoint) {
139
+ try {
140
+ const url = new URL(endpoint);
141
+ if (url.protocol !== "https:" && url.hostname !== "localhost" && url.hostname !== "127.0.0.1") {
142
+ return { name: "endpoint", ok: false, detail: `${endpoint} is not https` };
143
+ }
144
+ return { name: "endpoint", ok: true, detail: endpoint };
145
+ } catch {
146
+ return { name: "endpoint", ok: false, detail: `"${endpoint}" is not a URL` };
147
+ }
148
+ }
149
+ async function sendSmokeEvent(endpoint, writeKey, slug, cliVersion) {
150
+ const candidate = {
151
+ v: PROTOCOL_VERSION,
152
+ ...slug ? { to: slug } : {},
153
+ feedback: "hivenet doctor smoke event \u2014 verifying this integration; safe to mark as spam.",
154
+ category: "other",
155
+ subject: "hivenet doctor",
156
+ thread: { id: generateThreadId() },
157
+ client: { name: "hivenet-doctor", version: cliVersion },
158
+ consent: { telemetry: false }
159
+ };
160
+ const validated = validateFeedbackEvent(candidate);
161
+ if (!validated.ok) {
162
+ return { name: "smoke event", ok: false, detail: validated.errors.join("; ") };
163
+ }
164
+ const result = await postEvent({
165
+ endpoint,
166
+ ...writeKey ? { writeKey } : {},
167
+ event: validated.data,
168
+ timeoutMs: FETCH_TIMEOUT_MS
169
+ });
170
+ switch (result.kind) {
171
+ case "accepted":
172
+ return {
173
+ name: "smoke event",
174
+ ok: true,
175
+ detail: `delivered (${result.response.id})${result.response.guidance ? " \u2014 guidance configured \u2713" : ""}`
176
+ };
177
+ case "rejected":
178
+ return { name: "smoke event", ok: false, detail: `rejected: ${result.errors.join("; ")}` };
179
+ case "unreachable":
180
+ return { name: "smoke event", ok: false, detail: `endpoint unreachable: ${result.error}` };
181
+ }
182
+ }
183
+ function messageOf(error) {
184
+ if (error instanceof FeedbackError || error instanceof Error) {
185
+ return error.message;
186
+ }
187
+ return String(error);
188
+ }
189
+ function printDoctorHelp() {
190
+ console.log(`
191
+ Usage
192
+ $ npx hivenet doctor --to <slug|domain> [--smoke] [--json]
193
+ $ npx hivenet doctor --dsn <url> [--smoke] [--json]
194
+
195
+ Verify a hivenet integration end-to-end, the way an agent would resolve it:
196
+ registry entry or well-known descriptor (content type, schema), endpoint,
197
+ write key, and published categories. Ignores HIVENET_DSN and
198
+ feedback.config.json so a healthy local setup can't mask a broken public one.
199
+
200
+ Options
201
+ --to, -t <slug|domain> Registered slug, or a domain serving ${WELL_KNOWN_PATH}
202
+ --dsn <url> Explicit DSN: https://hv_pub_KEY@host/slug
203
+ --smoke Also send one disposable, clearly-labeled event
204
+ --json Machine-readable result
205
+ --help, -h This help
206
+ `);
207
+ }
208
+ export {
209
+ runDoctor
210
+ };