tunneling: brainstorm, Phase 1 relay server built + tested, boards + history updated
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
7c6fca54f9
commit
93f8c7bb33
|
|
@ -16,6 +16,7 @@ var NAV_DATA = {
|
||||||
path: 'outline/index.html',
|
path: 'outline/index.html',
|
||||||
children: [
|
children: [
|
||||||
{ title: 'rokojori-auth Restructure', path: 'outline/auth-restructure.html' },
|
{ title: 'rokojori-auth Restructure', path: 'outline/auth-restructure.html' },
|
||||||
|
{ title: 'User-Based Local Tunneling', path: 'outline/tunneling.html' },
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -49,6 +50,7 @@ var NAV_DATA = {
|
||||||
title: 'History',
|
title: 'History',
|
||||||
path: 'history/index.html',
|
path: 'history/index.html',
|
||||||
children: [
|
children: [
|
||||||
|
{ title: 'Wednesday, 16 July 2026', path: 'history/2026/07-July/16-Wednesday/index.html' },
|
||||||
{ title: 'Tuesday, 15 July 2026', path: 'history/2026/07-July/15-Tuesday/index.html' },
|
{ title: 'Tuesday, 15 July 2026', path: 'history/2026/07-July/15-Tuesday/index.html' },
|
||||||
{ title: 'Monday, 14 July 2026', path: 'history/2026/07-July/14-Monday/index.html' },
|
{ title: 'Monday, 14 July 2026', path: 'history/2026/07-July/14-Monday/index.html' },
|
||||||
{ title: 'Sunday, 13 July 2026', path: 'history/2026/07-July/13-Sunday/index.html' },
|
{ title: 'Sunday, 13 July 2026', path: 'history/2026/07-July/13-Sunday/index.html' },
|
||||||
|
|
|
||||||
|
|
@ -37,16 +37,6 @@
|
||||||
<div class="lane">
|
<div class="lane">
|
||||||
<div class="lane-header">Nice To Have</div>
|
<div class="lane-header">Nice To Have</div>
|
||||||
|
|
||||||
<task-item class="purple hide-content">
|
|
||||||
<task-title>Internet Tunnel / Port Relay</task-title>
|
|
||||||
<task-content>
|
|
||||||
Allow local devices — a Stable Diffusion instance, a local LLM, a GDScript language
|
|
||||||
server — to be accessible to authorised Roject users over the internet, routed through
|
|
||||||
the Roject server via a persistent WebSocket. Primary use case: phone as a thin client
|
|
||||||
while the main machine handles all heavy processing.
|
|
||||||
</task-content>
|
|
||||||
</task-item>
|
|
||||||
|
|
||||||
<task-item class="purple hide-content">
|
<task-item class="purple hide-content">
|
||||||
<task-title>Real-Time Multi-User Collaboration</task-title>
|
<task-title>Real-Time Multi-User Collaboration</task-title>
|
||||||
<task-content>
|
<task-content>
|
||||||
|
|
|
||||||
|
|
@ -113,6 +113,22 @@
|
||||||
<div class="lane">
|
<div class="lane">
|
||||||
<div class="lane-header">In Progress</div>
|
<div class="lane-header">In Progress</div>
|
||||||
|
|
||||||
|
<task-item class="yellow hide-content">
|
||||||
|
<task-title>rokojori-tunnel — Phase 2</task-title>
|
||||||
|
<task-content>
|
||||||
|
Phase 1 complete: relay server, CRUD tunnel API, WebSocket agent endpoint,
|
||||||
|
HTTP proxy, test agent script. Tested end-to-end with local LLM (gemma4-coding
|
||||||
|
on port 8900) — request relayed and response returned correctly.
|
||||||
|
Service lives at C:\rokojori\projects\web-projects\tunnel.
|
||||||
|
|
||||||
|
Phase 2 remaining:
|
||||||
|
— Allowed users list enforcement (multi-user private access)
|
||||||
|
— GET /api/tunnels/available with ?purpose= filter
|
||||||
|
— Public access mode (no auth required on proxy route)
|
||||||
|
— Roject LLM provider picker integrating the discovery API
|
||||||
|
</task-content>
|
||||||
|
</task-item>
|
||||||
|
|
||||||
<task-item class="yellow hide-content">
|
<task-item class="yellow hide-content">
|
||||||
<task-title>styles.rokojori.com — complete deployment</task-title>
|
<task-title>styles.rokojori.com — complete deployment</task-title>
|
||||||
<task-content>
|
<task-content>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,126 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Wednesday, 16 July 2026 — Roject</title>
|
||||||
|
<link rel="stylesheet" href="../../../../_assets_/styles.css">
|
||||||
|
<link rel="stylesheet" href="../../../../_assets_/nav.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="page">
|
||||||
|
|
||||||
|
<header>
|
||||||
|
<p class="date">Wednesday, 16 July 2026</p>
|
||||||
|
<h1>Session History</h1>
|
||||||
|
<p class="subtitle">Brainstormed and scaffolded rokojori-tunnel — user-based local tunneling service.</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2>What we built</h2>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h3>rokojori-tunnel — Phase 1 (relay server)</h3>
|
||||||
|
<p>
|
||||||
|
New standalone service at <code>C:\rokojori\projects\web-projects\tunnel</code>,
|
||||||
|
same stack as the rest of the ecosystem (Node.js, Express, ts-node, JSON file
|
||||||
|
storage, shared JWT verification).
|
||||||
|
</p>
|
||||||
|
<ul style="line-height:1.9;margin-top:0.75rem">
|
||||||
|
<li><code>source/server/db.ts</code> — JSON file storage for tunnel configs
|
||||||
|
(<code>build/data/db/tunnels.json</code>)</li>
|
||||||
|
<li><code>source/server/middleware/requireAuth.ts</code> — JWT verification,
|
||||||
|
same pattern as rokojori-auth</li>
|
||||||
|
<li><code>source/server/relay/TunnelRegistry.ts</code> — in-memory
|
||||||
|
<code>Map<tunnelId, WebSocket></code> of active agent connections</li>
|
||||||
|
<li><code>source/server/relay/pending.ts</code> — pending request callbacks
|
||||||
|
keyed by <code>reqId</code> for matching responses to waiting HTTP connections</li>
|
||||||
|
<li><code>source/server/routes/tunnels.ts</code> — full CRUD plus
|
||||||
|
<code>GET /api/tunnels/available?purpose=</code> discovery endpoint</li>
|
||||||
|
<li><code>source/server/routes/agent.ts</code> — WebSocket upgrade handler;
|
||||||
|
verifies JWT, confirms ownership, registers socket in registry</li>
|
||||||
|
<li><code>source/server/routes/proxy.ts</code> — <code>ALL /t/:tunnelId/*</code>
|
||||||
|
relay; soft auth check for access mode, raw body forwarding, 30s timeout</li>
|
||||||
|
<li><code>source/server/index.ts</code> — Express + HTTP server with manual
|
||||||
|
WebSocket upgrade routing; JSON middleware applied only to <code>/api</code>
|
||||||
|
routes so proxy receives raw body streams</li>
|
||||||
|
<li><code>scripts/test-agent.ts</code> — standalone Node.js agent for
|
||||||
|
testing before the Electron app exists; connects via WebSocket and forwards
|
||||||
|
inbound relay requests to a local port</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h3>End-to-end test — local LLM over tunnel</h3>
|
||||||
|
<p>
|
||||||
|
Registered a tunnel via <code>POST /api/tunnels</code>, started the test agent
|
||||||
|
forwarding to port 8900 (<code>gemma4-coding-Q4_K_M.gguf</code> running locally),
|
||||||
|
and sent an OpenAI-compatible <code>/v1/chat/completions</code> request through
|
||||||
|
the relay. Full round-trip succeeded — request forwarded, response relayed back,
|
||||||
|
streaming token count confirmed in the response.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h3>tunneling.html outline document</h3>
|
||||||
|
<p>
|
||||||
|
New plan document at <code>workspace/outline/tunneling.html</code> covering:
|
||||||
|
the relay mechanic (three-leg model, minimal inspection, raw byte forwarding),
|
||||||
|
access modes (private / public / password-protected), tunnel metadata shape and
|
||||||
|
purpose tags, all three components (relay server, Electron agent app, rokojori-auth
|
||||||
|
permission), API endpoint reference, Roject LLM provider integration example,
|
||||||
|
file structure, and phased implementation plan.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2>Key decisions</h2>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<p>
|
||||||
|
<strong>Separate service, not part of rokojori-auth.</strong>
|
||||||
|
Auth stays focused on identity. tunnel.rokojori.com is its own Express process
|
||||||
|
that verifies the shared JWT but owns all relay logic independently.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<p>
|
||||||
|
<strong>HTTP-only for Phase 1.</strong> Stable Diffusion (AUTOMATIC1111) and
|
||||||
|
OpenAI-compatible LLMs (Ollama, LM Studio) all speak HTTP with SSE streaming —
|
||||||
|
no WebSocket from the app side needed. Language servers use raw TCP and are out
|
||||||
|
of scope for now.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<p>
|
||||||
|
<strong>JSON envelope protocol for Phase 1.</strong> Requests and responses are
|
||||||
|
wrapped as <code>{ reqId, method, path, headers, body (base64) }</code> JSON
|
||||||
|
messages over the agent WebSocket. No raw byte framing needed at this stage;
|
||||||
|
the JSON envelope is simple enough and sidesteps binary WebSocket complexity.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<p>
|
||||||
|
<strong>No express.json() on proxy routes.</strong> The JSON middleware is applied
|
||||||
|
only to <code>/api/tunnels</code> so the proxy handler always receives a raw
|
||||||
|
readable body stream, regardless of content type.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
Roject — session history
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<script>var NAV_ROOT = '../../../../';</script>
|
||||||
|
<script src="../../../../_assets_/nav-data.js"></script>
|
||||||
|
<script src="../../../../_assets_/nav.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -19,6 +19,11 @@
|
||||||
<section>
|
<section>
|
||||||
<h2>2026 — July</h2>
|
<h2>2026 — July</h2>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h3><a href="2026/07-July/16-Wednesday/index.html">Wednesday, 16 July 2026</a></h3>
|
||||||
|
<p>Brainstormed and scaffolded rokojori-tunnel — user-based local tunneling service; Phase 1 relay server built and tested end-to-end with local LLM (gemma4-coding on port 8900).</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h3><a href="2026/07-July/15-Tuesday/index.html">Tuesday, 15 July 2026</a></h3>
|
<h3><a href="2026/07-July/15-Tuesday/index.html">Tuesday, 15 July 2026</a></h3>
|
||||||
<p>styles.rokojori.com built and deployed (font hosting, /get-font public CSS endpoint, CORS allowlist, auth-gated management pages); Roject Barlow font migrated from Google Fonts; old dashboard/groups/projects pages removed; add-subdomain deployment guide with live variable inputs.</p>
|
<p>styles.rokojori.com built and deployed (font hosting, /get-font public CSS endpoint, CORS allowlist, auth-gated management pages); Roject Barlow font migrated from Google Fonts; old dashboard/groups/projects pages removed; add-subdomain deployment guide with live variable inputs.</p>
|
||||||
|
|
|
||||||
|
|
@ -81,6 +81,23 @@
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h3>Ecosystem — tunnel.rokojori.com</h3>
|
||||||
|
<p>
|
||||||
|
A user-based local tunneling service (in development at
|
||||||
|
<code>C:\rokojori\projects\web-projects\tunnel</code>, planned to live at
|
||||||
|
<code>tunnel.rokojori.com</code>). Exposes local services — LLMs, Stable
|
||||||
|
Diffusion, language servers — to authorised rokojori users over the internet,
|
||||||
|
routed through a relay server via a persistent WebSocket agent connection.
|
||||||
|
Each tunnel is owned by a rokojori account and carries metadata (name, purpose
|
||||||
|
tag, description, access mode, allowed users) so apps can discover and select
|
||||||
|
tunnels via <code>GET /api/tunnels/available?purpose=</code>.
|
||||||
|
Phase 1 (relay server, CRUD API, HTTP proxy, test agent) is complete and
|
||||||
|
tested end-to-end. Phase 2 (multi-user access, discovery API, public mode)
|
||||||
|
is in progress.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h3>Ecosystem — styles.rokojori.com</h3>
|
<h3>Ecosystem — styles.rokojori.com</h3>
|
||||||
<p>
|
<p>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,435 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>User-Based Local Tunneling — Roject</title>
|
||||||
|
<link rel="stylesheet" href="../_assets_/styles.css">
|
||||||
|
<link rel="stylesheet" href="../_assets_/nav.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="page">
|
||||||
|
|
||||||
|
<header>
|
||||||
|
<p class="date">Plan — In Progress</p>
|
||||||
|
<h1>User-Based Local Tunneling</h1>
|
||||||
|
<p class="subtitle">
|
||||||
|
A generic rokojori-ecosystem service at <code>tunnel.rokojori.com</code> that lets
|
||||||
|
authenticated users expose local services to the internet — with access control,
|
||||||
|
metadata, and discovery built in from the start.
|
||||||
|
</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2>What it is</h2>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<p>
|
||||||
|
Local tunneling solves one problem: your local machine is running something useful
|
||||||
|
(a local LLM, a Stable Diffusion instance, a language server) but it is only
|
||||||
|
reachable on <code>localhost</code>. The tunnel service makes it reachable over
|
||||||
|
the internet by routing traffic through a relay server.
|
||||||
|
</p>
|
||||||
|
<p style="margin-top:0.75rem">
|
||||||
|
What makes this different from existing tools (ngrok, localtunnel, pinggy) is
|
||||||
|
identity. Every tunnel is owned by a rokojori user account. Access is controlled
|
||||||
|
per-user — you can keep a tunnel private, share it with specific people, or open
|
||||||
|
it to anyone with the link. Apps in the rokojori ecosystem can discover and use
|
||||||
|
tunnels via a standard API, so a user can point Roject's AI agent at their own
|
||||||
|
local LLM without configuring endpoints manually.
|
||||||
|
</p>
|
||||||
|
<p style="margin-top:0.75rem">
|
||||||
|
The service lives at <code>tunnel.rokojori.com</code> — a standalone Express app,
|
||||||
|
same stack as the rest of the ecosystem, verified via the shared JWT from
|
||||||
|
<code>account.rokojori.com</code>.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2>How the Relay Works</h2>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h3>The three-leg model</h3>
|
||||||
|
<p>
|
||||||
|
Traffic flows through three legs: browser/app → relay server → local agent → local service.
|
||||||
|
</p>
|
||||||
|
<pre style="margin-top:0.75rem"><code>App (Roject on phone)
|
||||||
|
→ HTTPS → tunnel.rokojori.com/t/<tunnelId>/v1/chat/completions
|
||||||
|
→ WebSocket envelope → Electron agent (on the home machine)
|
||||||
|
→ HTTP → localhost:11434/v1/chat/completions</code></pre>
|
||||||
|
<p style="margin-top:0.75rem">
|
||||||
|
The relay server is a consensual man-in-the-middle. Both sides connect to it
|
||||||
|
knowingly. The server's job is to forward traffic as transparently as possible —
|
||||||
|
inspect only what is required for routing, pass everything else as raw bytes.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h3>What must be inspected vs. what is forwarded raw</h3>
|
||||||
|
<p>
|
||||||
|
The server must read the HTTP request line (method, path) to strip the
|
||||||
|
<code>/t/<tunnelId></code> prefix, and must rewrite the <code>Host</code>
|
||||||
|
header. Everything else — body bytes, SSE chunks, binary image data — is piped
|
||||||
|
through opaque without parsing.
|
||||||
|
</p>
|
||||||
|
<p style="margin-top:0.75rem">
|
||||||
|
The WebSocket connection between the server and the agent requires a minimal
|
||||||
|
envelope to multiplex concurrent requests over one socket:
|
||||||
|
</p>
|
||||||
|
<pre style="margin-top:0.75rem"><code>[4 bytes: requestId] [raw HTTP bytes...]</code></pre>
|
||||||
|
<p style="margin-top:0.75rem">
|
||||||
|
The agent strips the 4-byte prefix, forwards the remaining bytes to
|
||||||
|
<code>localhost:<port></code>, and prepends the same requestId to the
|
||||||
|
response bytes before sending back. The server matches the response to the
|
||||||
|
waiting HTTP connection by requestId and flushes it.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h3>Streaming (SSE / chunked responses)</h3>
|
||||||
|
<p>
|
||||||
|
Local LLMs stream tokens via Server-Sent Events or chunked transfer encoding.
|
||||||
|
The agent forwards response bytes as they arrive — the relay server keeps the
|
||||||
|
HTTP response to the app open and flushes each chunk immediately. No buffering,
|
||||||
|
no full-response collection needed.
|
||||||
|
</p>
|
||||||
|
<p style="margin-top:0.75rem">
|
||||||
|
This means the app receives the stream in real time, exactly as it would if it
|
||||||
|
were calling the local service directly.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h3>WebSocket-based services — out of scope for v1</h3>
|
||||||
|
<p>
|
||||||
|
The primary target services (Stable Diffusion AUTOMATIC1111, Ollama, LM Studio,
|
||||||
|
OpenAI-compatible APIs) all speak HTTP with optional SSE streaming. None require
|
||||||
|
WebSocket from the app side. Language servers (C# OmniSharp, Godot LSP) use raw
|
||||||
|
TCP — a separate and harder problem, not planned for v1.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2>Access Modes</h2>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h3>Private</h3>
|
||||||
|
<p>
|
||||||
|
Only authenticated rokojori users in the tunnel's <code>allowedUserIds</code>
|
||||||
|
list (plus the owner) can send requests through it. The relay server verifies the
|
||||||
|
JWT on every inbound request. This is the default for new tunnels.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h3>Public (link-based)</h3>
|
||||||
|
<p>
|
||||||
|
Anyone with the tunnel URL can send requests — no authentication required.
|
||||||
|
Equivalent to what ngrok and localtunnel provide out of the box. Useful for quick
|
||||||
|
demos or sharing with clients who have no rokojori account.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h3>Password-protected (v2)</h3>
|
||||||
|
<p>
|
||||||
|
A single shared secret is required in an <code>X-Tunnel-Key</code> header or as
|
||||||
|
a query parameter. Simpler than full account management for trusted-but-anonymous
|
||||||
|
recipients. Not planned for v1.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2>Tunnel Metadata</h2>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<p>
|
||||||
|
Each tunnel carries metadata so that apps and users can discover, filter, and
|
||||||
|
select tunnels without prior knowledge of what is running behind them.
|
||||||
|
</p>
|
||||||
|
<pre style="margin-top:0.75rem"><code>{
|
||||||
|
"id": "uuid",
|
||||||
|
"name": "Josef's Ollama",
|
||||||
|
"description": "Local Ollama instance, llama3.2 and mistral available",
|
||||||
|
"purpose": "llm-openai-compatible",
|
||||||
|
"ownerId": "uuid",
|
||||||
|
"access": "private",
|
||||||
|
"allowedUserIds": ["uuid-alice", "uuid-bob"],
|
||||||
|
"localPort": 11434,
|
||||||
|
"active": true,
|
||||||
|
"createdAt": "2026-07-16T00:00:00Z"
|
||||||
|
}</code></pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h3>Purpose tags</h3>
|
||||||
|
<p>
|
||||||
|
The <code>purpose</code> field is a well-known tag that apps use to filter
|
||||||
|
relevant tunnels. Defined as a fixed set in the service:
|
||||||
|
</p>
|
||||||
|
<ul style="line-height:1.9;margin-top:0.75rem">
|
||||||
|
<li><code>llm-openai-compatible</code> — drop-in replacement for the OpenAI
|
||||||
|
<code>/v1/chat/completions</code> endpoint; works with any OpenAI-compatible API
|
||||||
|
(Ollama, LM Studio, etc.)</li>
|
||||||
|
<li><code>stable-diffusion</code> — AUTOMATIC1111 or ComfyUI image generation</li>
|
||||||
|
<li><code>general</code> — generic HTTP relay, no specific integration contract</li>
|
||||||
|
</ul>
|
||||||
|
<p style="margin-top:0.75rem">
|
||||||
|
More purpose tags are added as concrete integrations are built.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h3>Active vs. registered</h3>
|
||||||
|
<p>
|
||||||
|
A tunnel can be registered (config saved, metadata available for discovery) while
|
||||||
|
the agent is offline. The <code>active</code> flag reflects whether the agent
|
||||||
|
WebSocket is currently connected. Apps should check this before attempting to
|
||||||
|
send requests through a tunnel.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2>Components</h2>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h3>1 — tunnel.rokojori.com (relay server)</h3>
|
||||||
|
<p>
|
||||||
|
A standalone Express + Node.js service. Owns tunnel configs (JSON file storage),
|
||||||
|
the WebSocket server that agents connect to, and the HTTP proxy routes that apps
|
||||||
|
call.
|
||||||
|
</p>
|
||||||
|
<ul style="line-height:1.9;margin-top:0.75rem">
|
||||||
|
<li>Verifies JWTs from <code>account.rokojori.com</code> (shared <code>JWT_SECRET</code>)</li>
|
||||||
|
<li>Checks <code>tunnel:create</code> permission before allowing tunnel registration</li>
|
||||||
|
<li>Maintains an in-memory map of active agent WebSocket connections</li>
|
||||||
|
<li>Proxies inbound HTTP requests to the correct agent, streams responses back</li>
|
||||||
|
<li>Stores tunnel configs in <code>build/data/db/tunnels.json</code></li>
|
||||||
|
</ul>
|
||||||
|
<div class="tags">
|
||||||
|
<span class="tag">Node.js</span>
|
||||||
|
<span class="tag">Express</span>
|
||||||
|
<span class="tag">ts-node</span>
|
||||||
|
<span class="tag">ws (WebSocket)</span>
|
||||||
|
<span class="tag">tunnel.rokojori.com</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h3>2 — Electron agent app (local machine)</h3>
|
||||||
|
<p>
|
||||||
|
A small Electron desktop app that runs in the system tray. The user logs in once
|
||||||
|
via <code>account.rokojori.com</code> (same direct API call pattern as the Roject
|
||||||
|
Electron app). Tokens are stored in <code>userData/tokens.json</code>.
|
||||||
|
</p>
|
||||||
|
<p style="margin-top:0.75rem">
|
||||||
|
The user configures one or more tunnels — each with a name, purpose tag,
|
||||||
|
description, and local port. The app opens a persistent WebSocket to
|
||||||
|
<code>wss://tunnel.rokojori.com/api/agent/:tunnelId</code> for each active tunnel
|
||||||
|
and forwards inbound byte envelopes to the local port.
|
||||||
|
</p>
|
||||||
|
<ul style="line-height:1.9;margin-top:0.75rem">
|
||||||
|
<li>System tray icon — green dot when at least one tunnel is active</li>
|
||||||
|
<li>Main window: list of configured tunnels with active/inactive status</li>
|
||||||
|
<li>Add tunnel: name, purpose, description, local port, access mode, allowed users</li>
|
||||||
|
<li>Auto-reconnect on disconnect with exponential backoff</li>
|
||||||
|
<li>Start on boot option</li>
|
||||||
|
</ul>
|
||||||
|
<div class="tags">
|
||||||
|
<span class="tag">Electron</span>
|
||||||
|
<span class="tag">system tray</span>
|
||||||
|
<span class="tag">WebSocket agent</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h3>3 — rokojori-auth (permission only)</h3>
|
||||||
|
<p>
|
||||||
|
No tunnel logic lives in <code>rokojori-auth</code>. The only addition is a
|
||||||
|
<code>tunnel:create</code> permission added to the roles map. Users without this
|
||||||
|
permission are rejected by the relay server when attempting to register a tunnel.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2>API Endpoints — tunnel.rokojori.com</h2>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h3>Tunnel management (authenticated)</h3>
|
||||||
|
<pre><code>POST /api/tunnels — register a new tunnel config (requires tunnel:create)
|
||||||
|
GET /api/tunnels — list tunnels owned by the requesting user
|
||||||
|
GET /api/tunnels/available — list tunnels the user has access to (owned + allowed)
|
||||||
|
GET /api/tunnels/available?purpose= — same, filtered by purpose tag
|
||||||
|
GET /api/tunnels/:id — get one tunnel's metadata
|
||||||
|
PATCH /api/tunnels/:id — update name, description, access, allowedUserIds
|
||||||
|
DELETE /api/tunnels/:id — remove tunnel config (owner only)</code></pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h3>Agent connection (authenticated, WebSocket upgrade)</h3>
|
||||||
|
<pre><code>GET /api/agent/:tunnelId — WebSocket; agent connects here, stays open</code></pre>
|
||||||
|
<p style="margin-top:0.75rem">
|
||||||
|
On connection the server verifies the JWT, confirms the connecting user owns the
|
||||||
|
tunnel, and registers the socket in the in-memory map. On disconnect the tunnel
|
||||||
|
is marked inactive.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h3>Proxy route (access mode enforced)</h3>
|
||||||
|
<pre><code>ALL /t/:tunnelId/* — relay any HTTP method to the agent</code></pre>
|
||||||
|
<p style="margin-top:0.75rem">
|
||||||
|
For private tunnels the JWT is verified and the requesting user must be the owner
|
||||||
|
or appear in <code>allowedUserIds</code>. For public tunnels no auth is required.
|
||||||
|
If the tunnel is registered but the agent is offline, the server returns
|
||||||
|
<code>503 Service Unavailable</code>.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2>Integration in Apps — Roject Example</h2>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h3>The pattern</h3>
|
||||||
|
<p>
|
||||||
|
Any app in the rokojori ecosystem can query the tunnel service for tunnels
|
||||||
|
available to the current user, filtered by purpose. The app then uses the tunnel
|
||||||
|
URL as a standard HTTP endpoint — no special tunnel SDK required on the
|
||||||
|
app side.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h3>LLM provider selection</h3>
|
||||||
|
<p>
|
||||||
|
When a user configures an AI agent or task in Roject, they choose an LLM
|
||||||
|
provider. Providers come in two kinds:
|
||||||
|
</p>
|
||||||
|
<ul style="line-height:1.9;margin-top:0.75rem">
|
||||||
|
<li><strong>External</strong> — Anthropic, OpenAI, or any OpenAI-compatible
|
||||||
|
endpoint with an API key and base URL configured manually.</li>
|
||||||
|
<li><strong>Tunneled</strong> — a local service exposed via
|
||||||
|
<code>tunnel.rokojori.com</code>. The user clicks "Browse tunnels", Roject
|
||||||
|
fetches <code>GET /api/tunnels/available?purpose=llm-openai-compatible</code>
|
||||||
|
and shows a picker with each tunnel's name, description, owner, and active
|
||||||
|
status.</li>
|
||||||
|
</ul>
|
||||||
|
<p style="margin-top:0.75rem">
|
||||||
|
Once selected, Roject stores the <code>tunnelId</code> and constructs the
|
||||||
|
endpoint at runtime:
|
||||||
|
</p>
|
||||||
|
<pre style="margin-top:0.75rem"><code>https://tunnel.rokojori.com/t/<tunnelId>/v1/chat/completions</code></pre>
|
||||||
|
<p style="margin-top:0.75rem">
|
||||||
|
From Roject's perspective this is a standard OpenAI-compatible HTTP endpoint.
|
||||||
|
Streaming works identically — SSE chunks flow from the local LLM through
|
||||||
|
the relay to the Roject UI in real time. No code in Roject knows or cares that
|
||||||
|
a tunnel is involved.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h3>Family / team sharing</h3>
|
||||||
|
<p>
|
||||||
|
If a tunnel's <code>allowedUserIds</code> includes other rokojori accounts,
|
||||||
|
those users see the tunnel in their <code>/api/tunnels/available</code> response
|
||||||
|
too — even though they do not own it. A household can share one Stable
|
||||||
|
Diffusion machine; a small team can share one GPU server. The owner adds user IDs
|
||||||
|
via the Electron app or a future web UI on <code>tunnel.rokojori.com</code>.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2>File Structure — rokojori-tunnel</h2>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<pre><code>C:\rokojori\projects\web-projects\tunnel\
|
||||||
|
source/
|
||||||
|
server/
|
||||||
|
routes/
|
||||||
|
tunnels.ts — CRUD for tunnel configs
|
||||||
|
proxy.ts — ALL /t/:tunnelId/* relay handler
|
||||||
|
agent.ts — WebSocket upgrade endpoint for agents
|
||||||
|
middleware/
|
||||||
|
requireAuth.ts — JWT verification (shared pattern)
|
||||||
|
relay/
|
||||||
|
TunnelRegistry.ts — in-memory Map<tunnelId, AgentSocket>
|
||||||
|
db.ts — tunnel config storage (JSON files)
|
||||||
|
index.ts — Express entry point
|
||||||
|
electron-agent/
|
||||||
|
main.ts — Electron main process, tray setup
|
||||||
|
login-window.ts — direct API login, token storage
|
||||||
|
tray.ts — system tray icon and menu
|
||||||
|
agent/
|
||||||
|
TunnelAgent.ts — WebSocket connection + HTTP forwarding loop
|
||||||
|
AgentConfig.ts — local tunnel config (name, port, tunnelId)
|
||||||
|
build/
|
||||||
|
data/
|
||||||
|
db/
|
||||||
|
tunnels.json — registered tunnel configs
|
||||||
|
package.json
|
||||||
|
tsconfig.json
|
||||||
|
tsconfig.ts-node.json
|
||||||
|
.env</code></pre>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2>Environment Variables</h2>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<pre><code>JWT_SECRET=... — same shared value as all other rokojori services
|
||||||
|
PORT=3002 — or whichever port nginx proxies to</code></pre>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2>Phased Implementation</h2>
|
||||||
|
|
||||||
|
<div class="card" style="border-left: 3px solid #4caf50;">
|
||||||
|
<h3>Phase 1 — single user, HTTP only — <em>Complete</em></h3>
|
||||||
|
<ul style="line-height:1.9">
|
||||||
|
<li>Relay server built at <code>C:\rokojori\projects\web-projects\tunnel</code></li>
|
||||||
|
<li>Tunnel CRUD API, WebSocket agent endpoint, HTTP proxy route</li>
|
||||||
|
<li>Standalone test agent script (<code>scripts/test-agent.ts</code>)</li>
|
||||||
|
<li>Tested end-to-end: request relayed through tunnel to local <code>gemma4-coding</code> LLM on port 8900 and response returned correctly</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card" style="border-left: 3px solid #ffb300;">
|
||||||
|
<h3>Phase 2 — multi-user and discovery — <em>Next</em></h3>
|
||||||
|
<ul style="line-height:1.9">
|
||||||
|
<li>Allowed users list on tunnel config</li>
|
||||||
|
<li><code>GET /api/tunnels/available</code> endpoint with purpose filtering</li>
|
||||||
|
<li>Public access mode</li>
|
||||||
|
<li>Roject LLM provider picker integrating the discovery API</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h3>Phase 3 — polish</h3>
|
||||||
|
<ul style="line-height:1.9">
|
||||||
|
<li>Electron app: multiple tunnel configs, start on boot, tray status per tunnel</li>
|
||||||
|
<li>Web UI at <code>tunnel.rokojori.com</code> for managing tunnels without the desktop app</li>
|
||||||
|
<li>Per-tunnel request logs (count, last active timestamp)</li>
|
||||||
|
<li>Password-protected access mode</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
Roject — user-based local tunneling plan
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<script>var NAV_ROOT = '../';</script>
|
||||||
|
<script src="../_assets_/nav-data.js"></script>
|
||||||
|
<script src="../_assets_/nav.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Loading…
Reference in New Issue