@broberg/seti-server 0.1.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 ADDED
@@ -0,0 +1,36 @@
1
+ # @broberg/seti-server
2
+
3
+ Mountable [Hono](https://hono.dev) proxy router for **buddycloud.cc's SETI API v1** —
4
+ embed SET/SETI live streaming chat in a host app while the consumer token stays
5
+ server-side. The browser talks same-origin to the host (no CORS, host-app auth in
6
+ front, EventSource/fetch-SSE works with the host's cookies); this proxy injects the
7
+ bearer token server-to-server.
8
+
9
+ ```ts
10
+ import { createSetiProxy } from "@broberg/seti-server";
11
+
12
+ // Gate with YOUR auth first, then mount:
13
+ app.use("/api/seti/*", hostAuthMiddleware);
14
+ app.route(
15
+ "/api/seti",
16
+ createSetiProxy({
17
+ cloudUrl: process.env.SETI_CLOUD_URL!, // e.g. https://buddycloud.cc
18
+ token: process.env.SETI_TOKEN!, // a BUDDY_SETI_TOKENS consumer token
19
+ }),
20
+ );
21
+ ```
22
+
23
+ ## Routes (1:1 against `{cloudUrl}/api/seti/v1/*`)
24
+
25
+ | Route | Method | Purpose |
26
+ | --- | --- | --- |
27
+ | `/sessions` | GET | Fleet roster: `{ edges: [{ edgeId, connected, tmuxSessions, sessions }] }`. `tmuxSessions` are the **streamable** units — use them as the `session` param. |
28
+ | `/stream?edge=&session=` | GET | SSE pass-through: `hello` / `frame` (full pane snapshot) / `ping`. |
29
+ | `/input` | POST | `{ edge, session, text? \| key? }` — text lines or nav-keys (`SETI_KEYS`). |
30
+
31
+ Pairs with [`@broberg/seti-client`](https://www.npmjs.com/package/@broberg/seti-client)
32
+ (typed client + frame-merge engine + Preact `<SetiChat>` component).
33
+
34
+ Peer dependency: `hono ^4`. No runtime dependencies.
35
+
36
+ MIT © broberg.ai
package/dist/index.cjs ADDED
@@ -0,0 +1,84 @@
1
+ 'use strict';
2
+
3
+ var hono = require('hono');
4
+
5
+ // src/index.ts
6
+ var SETI_KEYS = [
7
+ "Escape",
8
+ "Up",
9
+ "Down",
10
+ "Left",
11
+ "Right",
12
+ "Enter",
13
+ "BSpace",
14
+ "Tab"
15
+ ];
16
+ function createSetiProxy(opts) {
17
+ const base = opts.cloudUrl.replace(/\/$/, "");
18
+ const doFetch = opts.fetch ?? globalThis.fetch.bind(globalThis);
19
+ const auth = { Authorization: `Bearer ${opts.token}` };
20
+ const app = new hono.Hono();
21
+ app.get("/sessions", async (c) => {
22
+ try {
23
+ const res = await doFetch(`${base}/api/seti/v1/sessions`, { headers: auth });
24
+ const body = await res.text();
25
+ return new Response(body, {
26
+ status: res.status,
27
+ headers: { "content-type": "application/json" }
28
+ });
29
+ } catch {
30
+ return c.json({ edges: [], error: "cloud_unreachable" }, 502);
31
+ }
32
+ });
33
+ app.get("/stream", async (c) => {
34
+ const edge = c.req.query("edge") ?? "";
35
+ const session = c.req.query("session") ?? "";
36
+ if (!edge || !session) return c.json({ error: "edge_and_session_required" }, 400);
37
+ let upstream;
38
+ try {
39
+ upstream = await doFetch(
40
+ `${base}/api/seti/v1/stream?edge=${encodeURIComponent(edge)}&session=${encodeURIComponent(session)}`,
41
+ // Propagate the client abort so the upstream attach heartbeat stops
42
+ // (and the edge's capture loop expires) when the viewer leaves.
43
+ { headers: auth, signal: c.req.raw.signal }
44
+ );
45
+ } catch {
46
+ return c.json({ error: "cloud_unreachable" }, 502);
47
+ }
48
+ if (!upstream.ok || !upstream.body) {
49
+ return c.json({ error: `upstream_${upstream.status}` }, 502);
50
+ }
51
+ return new Response(upstream.body, {
52
+ status: 200,
53
+ headers: {
54
+ "content-type": "text/event-stream",
55
+ "cache-control": "no-cache",
56
+ connection: "keep-alive",
57
+ "x-accel-buffering": "no"
58
+ }
59
+ });
60
+ });
61
+ app.post("/input", async (c) => {
62
+ const body = await c.req.text();
63
+ try {
64
+ const res = await doFetch(`${base}/api/seti/v1/input`, {
65
+ method: "POST",
66
+ headers: { ...auth, "content-type": "application/json" },
67
+ body
68
+ });
69
+ const out = await res.text();
70
+ return new Response(out, {
71
+ status: res.status,
72
+ headers: { "content-type": "application/json" }
73
+ });
74
+ } catch {
75
+ return c.json({ ok: false, error: "cloud_unreachable" }, 502);
76
+ }
77
+ });
78
+ return app;
79
+ }
80
+
81
+ exports.SETI_KEYS = SETI_KEYS;
82
+ exports.createSetiProxy = createSetiProxy;
83
+ //# sourceMappingURL=index.cjs.map
84
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts"],"names":["Hono"],"mappings":"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","file":"index.cjs","sourcesContent":["import { Hono } from \"hono\";\n\n/**\n * @broberg/seti-server — mountable Hono proxy for buddycloud.cc's SETI API v1.\n *\n * The host app gates with its OWN auth, then mounts:\n *\n * app.use('/api/seti/*', hostAuthMiddleware);\n * app.route('/api/seti', createSetiProxy({ cloudUrl, token }));\n *\n * The consumer token never reaches the browser: the page talks same-origin to\n * the host (so EventSource works with the host's cookie auth and no CORS is\n * needed), and this proxy injects the bearer token server-to-server.\n *\n * Routes (1:1 against `${cloudUrl}/api/seti/v1/*`):\n * GET /sessions — fleet roster ({ edges: [{ edgeId, connected, tmuxSessions, sessions }] })\n * GET /stream — SSE pass-through (hello / frame / ping events)\n * POST /input — { edge, session, text? | key? }\n */\nexport interface SetiProxyOptions {\n /** The buddy cloud hub, e.g. \"https://buddycloud.cc\". */\n cloudUrl: string;\n /** A SETI consumer token (one of the cloud's BUDDY_SETI_TOKENS). */\n token: string;\n /** Override fetch (tests). Defaults to globalThis.fetch. */\n fetch?: typeof fetch;\n}\n\n/** tmux key names accepted by POST /input's `key` field. */\nexport const SETI_KEYS = [\n \"Escape\",\n \"Up\",\n \"Down\",\n \"Left\",\n \"Right\",\n \"Enter\",\n \"BSpace\",\n \"Tab\",\n] as const;\n\nexport function createSetiProxy(opts: SetiProxyOptions): Hono {\n const base = opts.cloudUrl.replace(/\\/$/, \"\");\n const doFetch = opts.fetch ?? globalThis.fetch.bind(globalThis);\n const auth = { Authorization: `Bearer ${opts.token}` };\n const app = new Hono();\n\n app.get(\"/sessions\", async (c) => {\n try {\n const res = await doFetch(`${base}/api/seti/v1/sessions`, { headers: auth });\n const body = await res.text();\n return new Response(body, {\n status: res.status,\n headers: { \"content-type\": \"application/json\" },\n });\n } catch {\n return c.json({ edges: [], error: \"cloud_unreachable\" }, 502);\n }\n });\n\n app.get(\"/stream\", async (c) => {\n const edge = c.req.query(\"edge\") ?? \"\";\n const session = c.req.query(\"session\") ?? \"\";\n if (!edge || !session) return c.json({ error: \"edge_and_session_required\" }, 400);\n let upstream: Response;\n try {\n upstream = await doFetch(\n `${base}/api/seti/v1/stream?edge=${encodeURIComponent(edge)}&session=${encodeURIComponent(session)}`,\n // Propagate the client abort so the upstream attach heartbeat stops\n // (and the edge's capture loop expires) when the viewer leaves.\n { headers: auth, signal: c.req.raw.signal },\n );\n } catch {\n return c.json({ error: \"cloud_unreachable\" }, 502);\n }\n if (!upstream.ok || !upstream.body) {\n return c.json({ error: `upstream_${upstream.status}` }, 502);\n }\n return new Response(upstream.body, {\n status: 200,\n headers: {\n \"content-type\": \"text/event-stream\",\n \"cache-control\": \"no-cache\",\n connection: \"keep-alive\",\n \"x-accel-buffering\": \"no\",\n },\n });\n });\n\n app.post(\"/input\", async (c) => {\n const body = await c.req.text();\n try {\n const res = await doFetch(`${base}/api/seti/v1/input`, {\n method: \"POST\",\n headers: { ...auth, \"content-type\": \"application/json\" },\n body,\n });\n const out = await res.text();\n return new Response(out, {\n status: res.status,\n headers: { \"content-type\": \"application/json\" },\n });\n } catch {\n return c.json({ ok: false, error: \"cloud_unreachable\" }, 502);\n }\n });\n\n return app;\n}\n"]}
@@ -0,0 +1,32 @@
1
+ import { Hono } from 'hono';
2
+
3
+ /**
4
+ * @broberg/seti-server — mountable Hono proxy for buddycloud.cc's SETI API v1.
5
+ *
6
+ * The host app gates with its OWN auth, then mounts:
7
+ *
8
+ * app.use('/api/seti/*', hostAuthMiddleware);
9
+ * app.route('/api/seti', createSetiProxy({ cloudUrl, token }));
10
+ *
11
+ * The consumer token never reaches the browser: the page talks same-origin to
12
+ * the host (so EventSource works with the host's cookie auth and no CORS is
13
+ * needed), and this proxy injects the bearer token server-to-server.
14
+ *
15
+ * Routes (1:1 against `${cloudUrl}/api/seti/v1/*`):
16
+ * GET /sessions — fleet roster ({ edges: [{ edgeId, connected, tmuxSessions, sessions }] })
17
+ * GET /stream — SSE pass-through (hello / frame / ping events)
18
+ * POST /input — { edge, session, text? | key? }
19
+ */
20
+ interface SetiProxyOptions {
21
+ /** The buddy cloud hub, e.g. "https://buddycloud.cc". */
22
+ cloudUrl: string;
23
+ /** A SETI consumer token (one of the cloud's BUDDY_SETI_TOKENS). */
24
+ token: string;
25
+ /** Override fetch (tests). Defaults to globalThis.fetch. */
26
+ fetch?: typeof fetch;
27
+ }
28
+ /** tmux key names accepted by POST /input's `key` field. */
29
+ declare const SETI_KEYS: readonly ["Escape", "Up", "Down", "Left", "Right", "Enter", "BSpace", "Tab"];
30
+ declare function createSetiProxy(opts: SetiProxyOptions): Hono;
31
+
32
+ export { SETI_KEYS, type SetiProxyOptions, createSetiProxy };
@@ -0,0 +1,32 @@
1
+ import { Hono } from 'hono';
2
+
3
+ /**
4
+ * @broberg/seti-server — mountable Hono proxy for buddycloud.cc's SETI API v1.
5
+ *
6
+ * The host app gates with its OWN auth, then mounts:
7
+ *
8
+ * app.use('/api/seti/*', hostAuthMiddleware);
9
+ * app.route('/api/seti', createSetiProxy({ cloudUrl, token }));
10
+ *
11
+ * The consumer token never reaches the browser: the page talks same-origin to
12
+ * the host (so EventSource works with the host's cookie auth and no CORS is
13
+ * needed), and this proxy injects the bearer token server-to-server.
14
+ *
15
+ * Routes (1:1 against `${cloudUrl}/api/seti/v1/*`):
16
+ * GET /sessions — fleet roster ({ edges: [{ edgeId, connected, tmuxSessions, sessions }] })
17
+ * GET /stream — SSE pass-through (hello / frame / ping events)
18
+ * POST /input — { edge, session, text? | key? }
19
+ */
20
+ interface SetiProxyOptions {
21
+ /** The buddy cloud hub, e.g. "https://buddycloud.cc". */
22
+ cloudUrl: string;
23
+ /** A SETI consumer token (one of the cloud's BUDDY_SETI_TOKENS). */
24
+ token: string;
25
+ /** Override fetch (tests). Defaults to globalThis.fetch. */
26
+ fetch?: typeof fetch;
27
+ }
28
+ /** tmux key names accepted by POST /input's `key` field. */
29
+ declare const SETI_KEYS: readonly ["Escape", "Up", "Down", "Left", "Right", "Enter", "BSpace", "Tab"];
30
+ declare function createSetiProxy(opts: SetiProxyOptions): Hono;
31
+
32
+ export { SETI_KEYS, type SetiProxyOptions, createSetiProxy };
package/dist/index.js ADDED
@@ -0,0 +1,81 @@
1
+ import { Hono } from 'hono';
2
+
3
+ // src/index.ts
4
+ var SETI_KEYS = [
5
+ "Escape",
6
+ "Up",
7
+ "Down",
8
+ "Left",
9
+ "Right",
10
+ "Enter",
11
+ "BSpace",
12
+ "Tab"
13
+ ];
14
+ function createSetiProxy(opts) {
15
+ const base = opts.cloudUrl.replace(/\/$/, "");
16
+ const doFetch = opts.fetch ?? globalThis.fetch.bind(globalThis);
17
+ const auth = { Authorization: `Bearer ${opts.token}` };
18
+ const app = new Hono();
19
+ app.get("/sessions", async (c) => {
20
+ try {
21
+ const res = await doFetch(`${base}/api/seti/v1/sessions`, { headers: auth });
22
+ const body = await res.text();
23
+ return new Response(body, {
24
+ status: res.status,
25
+ headers: { "content-type": "application/json" }
26
+ });
27
+ } catch {
28
+ return c.json({ edges: [], error: "cloud_unreachable" }, 502);
29
+ }
30
+ });
31
+ app.get("/stream", async (c) => {
32
+ const edge = c.req.query("edge") ?? "";
33
+ const session = c.req.query("session") ?? "";
34
+ if (!edge || !session) return c.json({ error: "edge_and_session_required" }, 400);
35
+ let upstream;
36
+ try {
37
+ upstream = await doFetch(
38
+ `${base}/api/seti/v1/stream?edge=${encodeURIComponent(edge)}&session=${encodeURIComponent(session)}`,
39
+ // Propagate the client abort so the upstream attach heartbeat stops
40
+ // (and the edge's capture loop expires) when the viewer leaves.
41
+ { headers: auth, signal: c.req.raw.signal }
42
+ );
43
+ } catch {
44
+ return c.json({ error: "cloud_unreachable" }, 502);
45
+ }
46
+ if (!upstream.ok || !upstream.body) {
47
+ return c.json({ error: `upstream_${upstream.status}` }, 502);
48
+ }
49
+ return new Response(upstream.body, {
50
+ status: 200,
51
+ headers: {
52
+ "content-type": "text/event-stream",
53
+ "cache-control": "no-cache",
54
+ connection: "keep-alive",
55
+ "x-accel-buffering": "no"
56
+ }
57
+ });
58
+ });
59
+ app.post("/input", async (c) => {
60
+ const body = await c.req.text();
61
+ try {
62
+ const res = await doFetch(`${base}/api/seti/v1/input`, {
63
+ method: "POST",
64
+ headers: { ...auth, "content-type": "application/json" },
65
+ body
66
+ });
67
+ const out = await res.text();
68
+ return new Response(out, {
69
+ status: res.status,
70
+ headers: { "content-type": "application/json" }
71
+ });
72
+ } catch {
73
+ return c.json({ ok: false, error: "cloud_unreachable" }, 502);
74
+ }
75
+ });
76
+ return app;
77
+ }
78
+
79
+ export { SETI_KEYS, createSetiProxy };
80
+ //# sourceMappingURL=index.js.map
81
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["import { Hono } from \"hono\";\n\n/**\n * @broberg/seti-server — mountable Hono proxy for buddycloud.cc's SETI API v1.\n *\n * The host app gates with its OWN auth, then mounts:\n *\n * app.use('/api/seti/*', hostAuthMiddleware);\n * app.route('/api/seti', createSetiProxy({ cloudUrl, token }));\n *\n * The consumer token never reaches the browser: the page talks same-origin to\n * the host (so EventSource works with the host's cookie auth and no CORS is\n * needed), and this proxy injects the bearer token server-to-server.\n *\n * Routes (1:1 against `${cloudUrl}/api/seti/v1/*`):\n * GET /sessions — fleet roster ({ edges: [{ edgeId, connected, tmuxSessions, sessions }] })\n * GET /stream — SSE pass-through (hello / frame / ping events)\n * POST /input — { edge, session, text? | key? }\n */\nexport interface SetiProxyOptions {\n /** The buddy cloud hub, e.g. \"https://buddycloud.cc\". */\n cloudUrl: string;\n /** A SETI consumer token (one of the cloud's BUDDY_SETI_TOKENS). */\n token: string;\n /** Override fetch (tests). Defaults to globalThis.fetch. */\n fetch?: typeof fetch;\n}\n\n/** tmux key names accepted by POST /input's `key` field. */\nexport const SETI_KEYS = [\n \"Escape\",\n \"Up\",\n \"Down\",\n \"Left\",\n \"Right\",\n \"Enter\",\n \"BSpace\",\n \"Tab\",\n] as const;\n\nexport function createSetiProxy(opts: SetiProxyOptions): Hono {\n const base = opts.cloudUrl.replace(/\\/$/, \"\");\n const doFetch = opts.fetch ?? globalThis.fetch.bind(globalThis);\n const auth = { Authorization: `Bearer ${opts.token}` };\n const app = new Hono();\n\n app.get(\"/sessions\", async (c) => {\n try {\n const res = await doFetch(`${base}/api/seti/v1/sessions`, { headers: auth });\n const body = await res.text();\n return new Response(body, {\n status: res.status,\n headers: { \"content-type\": \"application/json\" },\n });\n } catch {\n return c.json({ edges: [], error: \"cloud_unreachable\" }, 502);\n }\n });\n\n app.get(\"/stream\", async (c) => {\n const edge = c.req.query(\"edge\") ?? \"\";\n const session = c.req.query(\"session\") ?? \"\";\n if (!edge || !session) return c.json({ error: \"edge_and_session_required\" }, 400);\n let upstream: Response;\n try {\n upstream = await doFetch(\n `${base}/api/seti/v1/stream?edge=${encodeURIComponent(edge)}&session=${encodeURIComponent(session)}`,\n // Propagate the client abort so the upstream attach heartbeat stops\n // (and the edge's capture loop expires) when the viewer leaves.\n { headers: auth, signal: c.req.raw.signal },\n );\n } catch {\n return c.json({ error: \"cloud_unreachable\" }, 502);\n }\n if (!upstream.ok || !upstream.body) {\n return c.json({ error: `upstream_${upstream.status}` }, 502);\n }\n return new Response(upstream.body, {\n status: 200,\n headers: {\n \"content-type\": \"text/event-stream\",\n \"cache-control\": \"no-cache\",\n connection: \"keep-alive\",\n \"x-accel-buffering\": \"no\",\n },\n });\n });\n\n app.post(\"/input\", async (c) => {\n const body = await c.req.text();\n try {\n const res = await doFetch(`${base}/api/seti/v1/input`, {\n method: \"POST\",\n headers: { ...auth, \"content-type\": \"application/json\" },\n body,\n });\n const out = await res.text();\n return new Response(out, {\n status: res.status,\n headers: { \"content-type\": \"application/json\" },\n });\n } catch {\n return c.json({ ok: false, error: \"cloud_unreachable\" }, 502);\n }\n });\n\n return app;\n}\n"]}
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "@broberg/seti-server",
3
+ "version": "0.1.0",
4
+ "description": "Mountable Hono proxy router for buddycloud.cc's SETI API v1 — embed SET/SETI live streaming chat in a host app while the consumer token stays server-side (no CORS, host-app auth in front).",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "sideEffects": false,
8
+ "files": ["dist", "README.md"],
9
+ "main": "./dist/index.cjs",
10
+ "module": "./dist/index.js",
11
+ "types": "./dist/index.d.ts",
12
+ "exports": {
13
+ ".": {
14
+ "types": "./dist/index.d.ts",
15
+ "import": "./dist/index.js",
16
+ "require": "./dist/index.cjs"
17
+ }
18
+ },
19
+ "scripts": {
20
+ "build": "tsup",
21
+ "test": "vitest run",
22
+ "typecheck": "tsc --noEmit"
23
+ },
24
+ "peerDependencies": {
25
+ "hono": "^4.0.0"
26
+ },
27
+ "devDependencies": {
28
+ "hono": "^4.6.14",
29
+ "tsup": "^8.3.0",
30
+ "typescript": "^5.6.0",
31
+ "vitest": "^2.1.0"
32
+ },
33
+ "keywords": [
34
+ "seti",
35
+ "set",
36
+ "streaming",
37
+ "terminal",
38
+ "tmux",
39
+ "sse",
40
+ "proxy",
41
+ "hono",
42
+ "buddycloud",
43
+ "broberg"
44
+ ],
45
+ "repository": {
46
+ "type": "git",
47
+ "url": "https://github.com/broberg-ai/components",
48
+ "directory": "packages/seti-server"
49
+ },
50
+ "publishConfig": { "access": "public" }
51
+ }