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',
|
||||
children: [
|
||||
{ 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',
|
||||
path: 'history/index.html',
|
||||
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: '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' },
|
||||
|
|
|
|||
|
|
@ -37,16 +37,6 @@
|
|||
<div class="lane">
|
||||
<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-title>Real-Time Multi-User Collaboration</task-title>
|
||||
<task-content>
|
||||
|
|
|
|||
|
|
@ -113,6 +113,22 @@
|
|||
<div class="lane">
|
||||
<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-title>styles.rokojori.com — complete deployment</task-title>
|
||||
<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>
|
||||
<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">
|
||||
<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>
|
||||
|
|
|
|||
|
|
@ -81,6 +81,23 @@
|
|||
</p>
|
||||
</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">
|
||||
<h3>Ecosystem — styles.rokojori.com</h3>
|
||||
<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