Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,12 @@ WORMKEY_EDGE_URL=ws://localhost:3002/tunnel
WORMKEY_PUBLIC_BASE_URL=http://localhost:3002
WORMKEY_EDGE_BASE_URL=ws://localhost:3002

# Production control plane
# WORMKEY_PUBLIC_BASE_URL=https://wormkey.run
# Gateway (Subdomain Routing)
# Set your domain to enable wildcard subdomains (e.g. slug.wormkey.run)
WORMKEY_DOMAIN=wormkey.run

# Production control plane (Management URLs)
# WORMKEY_PUBLIC_BASE_URL=https://t.wormkey.run
# WORMKEY_EDGE_BASE_URL=wss://wormkey-gateway.onrender.com

# Website: global mascot download counter (Upstash Redis)
Expand Down
6 changes: 3 additions & 3 deletions TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,9 @@ The hard conceptual problem is solved. Now we structure the next phases so Wormk

**Gateway changes:**
- [x] Extract slug from `Host` header
- [ ] Remove cookie-based routing fallback (once wildcard live)
- [ ] Only use cookie for owner identity
- [ ] Remove query-based routing entirely once wildcard is live
- [x] Remove cookie-based routing fallback (once wildcard live)
- [x] Only use cookie for owner identity
- [x] Remove query-based routing entirely once wildcard is live

This moves Wormkey from dev tool to internet product.

Expand Down
3 changes: 2 additions & 1 deletion packages/cli/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export interface CreateSessionResponse {

export async function createSession(
controlPlaneUrl: string,
options: { port: number; auth?: boolean; expires?: string }
options: { port: number; auth?: boolean; expires?: string; forcePath?: boolean }
): Promise<CreateSessionResponse> {
const res = await fetch(`${controlPlaneUrl}/sessions`, {
method: "POST",
Expand All @@ -26,6 +26,7 @@ export async function createSession(
port: options.port,
authMode: options.auth ? "basic" : "none",
expiresIn: options.expires ?? "24h",
forcePath: options.forcePath,
}),
});

Expand Down
6 changes: 4 additions & 2 deletions packages/cli/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ program
.command("http <port>")
.description("Expose local port via wormhole")
.option("--auth", "Enable basic auth (prints username/password)")
.option("--path", "Force path-based routing (/s/slug/)")
.option("--expires <duration>", "Session expiry (e.g. 30m, 1h, 24h)", "24h")
.option("--control-plane <url>", "Control plane URL")
.option("--edge <url>", "Edge tunnel URL")
Expand All @@ -105,15 +106,16 @@ program
: "wss://t.wormkey.run/tunnel";

const controlPlane =
process.env.WORMKEY_CONTROL_PLANE_URL ??
opts.controlPlane ??
process.env.WORMKEY_CONTROL_PLANE_URL ??
defaultControlPlane;
console.error("Control plane:", controlPlane);

const session = await createSession(controlPlane, {
port: portNum,
auth: opts.auth,
expires: opts.expires,
forcePath: opts.path,
});

if (opts.auth && session.username && session.password) {
Expand All @@ -124,8 +126,8 @@ program
}

const edgeUrl =
process.env.WORMKEY_EDGE_URL ??
opts.edge ??
process.env.WORMKEY_EDGE_URL ??
session.edgeUrl ??
defaultEdge;
console.error("Edge tunnel:", edgeUrl);
Expand Down
22 changes: 15 additions & 7 deletions packages/control-plane/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import cors from "@fastify/cors";

const ADJECTIVES = [
"quiet", "bold", "swift", "calm", "bright", "soft", "warm", "cool",
"deep", "flat", "wild", "mild", "dark", "pale", "pure", "rare", "max" ,
"deep", "flat", "wild", "mild", "dark", "pale", "pure", "rare", "max",
];
const NOUNS = [
"lime", "mint", "sage", "rose", "sky", "sea", "sand", "snow",
Expand Down Expand Up @@ -80,22 +80,30 @@ async function main() {
}

fastify.post<{
Body: { port?: number; authMode?: string; expiresIn?: string };
Body: { port?: number; authMode?: string; expiresIn?: string; slug?: string; forcePath?: boolean };
}>("/sessions", async (req, reply) => {
const { port = 3000, authMode = "none", expiresIn = "24h" } = req.body ?? {};
const { port = 3000, authMode = "none", expiresIn = "24h", slug: requestedSlug, forcePath = false } = req.body ?? {};

const slug = randomSlug();
const slug = requestedSlug || randomSlug();
const ownerToken = randomToken();
const sessionToken = `${slug}.${ownerToken}`;
const sessionId = `sess_${randomToken()}`;

// All URLs derived from env (canonical origin). Never use request host.
const publicBase = PUBLIC_BASE_URL.replace(/\/$/, "");
const edgeBase = EDGE_BASE_URL.replace(/\/$/, "");
const publicUrl = `${publicBase}/s/${slug}`;

// Choose URL format: Subdomain by default if domain is set, unless forcePath is specified
const domain = process.env.WORMKEY_DOMAIN;
let publicUrl = `${publicBase}/s/${slug}`;
if (domain && !forcePath) {
const protocol = publicBase.split(":")[0];
publicUrl = `${protocol}://${slug}.${domain}`;
}

const edgeUrl = `${edgeBase}/tunnel`;
const ownerUrl = `${publicBase}/.wormkey/owner?slug=${slug}&token=${ownerToken}`;
const overlayScriptUrl = `${publicBase}/.wormkey/overlay.js?slug=${slug}`;
const ownerUrl = `${publicUrl}/.wormkey/owner?token=${ownerToken}`;
const overlayScriptUrl = `${publicUrl}/.wormkey/overlay.js`;

const expiresMs =
expiresIn.endsWith("m")
Expand Down
64 changes: 29 additions & 35 deletions packages/gateway/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@
package main

import (
_ "embed"
"bufio"
"bytes"
"crypto/rand"
_ "embed"
"encoding/binary"
"encoding/hex"
"encoding/json"
Expand Down Expand Up @@ -185,18 +185,38 @@ func randomSecret(n int) string {
}

// extractSlugFromHost extracts slug from host like "quiet-lime-82.wormkey.run" or "quiet-lime-82.wormkey.run:3002".
// Assumes format: slug.wormkey.run (at least 3 dot-separated parts).
func extractSlugFromHost(host string) string {
host = strings.Split(host, ":")[0] // strip port
parts := strings.Split(host, ".")
if len(parts) < 3 {
domain := os.Getenv("WORMKEY_DOMAIN")
if domain == "" {
// Fallback for local development if domain not set: assumes format slug.wormkey.run (at least 3 parts)
parts := strings.Split(host, ".")
if len(parts) >= 3 {
return parts[0]
}
return ""
}

// Wildcard: support both slug.domain and subdomains of subdomains if domain is set correctly
domain = strings.Split(domain, ":")[0] // strip port from domain
if !strings.HasSuffix(host, "."+domain) {
return ""
}
return parts[0]
return strings.TrimSuffix(host, "."+domain)
}

func resolveSlug(r *http.Request) string {
// 1. Path-based: /s/:slug (no wildcard TLS needed)
// 1. Query-based (Highest priority for management tools/claims)
if s := r.URL.Query().Get("slug"); s != "" {
return s
}

// 2. Host-based (Primary for tunnels) - e.g. slug.wormkey.run
if s := extractSlugFromHost(r.Host); s != "" {
return s
}

// 3. Path-based (Legacy fallback) - e.g. wormkey.run/s/slug
if strings.HasPrefix(r.URL.Path, "/s/") {
rest := r.URL.Path[3:] // skip "/s/"
idx := strings.Index(rest, "/")
Expand All @@ -218,22 +238,7 @@ func resolveSlug(r *http.Request) string {
return slug
}
}
// 2. Query fallback (?slug=)
slug := r.URL.Query().Get("slug")
if slug != "" {
return slug
}
// 3. Cookie (for asset requests like /_next/... or /assets/...)
if c, err := r.Cookie("wormkey_slug"); err == nil && c.Value != "" {
return c.Value
}
if c, err := r.Cookie("wormkey"); err == nil && c.Value != "" {
return c.Value
}
// 4. Host-based (slug.wormkey.run)
if s := extractSlugFromHost(r.Host); s != "" {
return s
}

return ""
}

Expand Down Expand Up @@ -520,10 +525,8 @@ func main() {
http.Error(w, "Invalid owner token", 401)
return
}
setCookie(w, "wormkey_slug", slug, false)
setCookie(w, "wormkey", slug, false)
setCookie(w, "wormkey_owner", token, true)
http.Redirect(w, r, "/s/"+slug, http.StatusFound)
http.Redirect(w, r, "/", http.StatusFound)
})

mux.HandleFunc("/.wormkey/me", func(w http.ResponseWriter, r *http.Request) {
Expand Down Expand Up @@ -848,10 +851,6 @@ func handleTunnel(tunnels *sync.Map, closedSlugs *sync.Map, controlPlaneURL stri
sc.w.Header().Set(k, v)
}
}
if sc.setCookie != "" {
// wormkey_slug ensures asset requests (/_next/..., /assets/...) route correctly
sc.w.Header().Add("Set-Cookie", "wormkey_slug="+sc.setCookie+"; Path=/; SameSite=Lax")
}
sc.w.WriteHeader(status)
if sc.flusher != nil {
sc.flusher.Flush()
Expand Down Expand Up @@ -887,7 +886,6 @@ func handleTunnel(tunnels *sync.Map, closedSlugs *sync.Map, controlPlaneURL stri

func handleProxy(tunnels *sync.Map, controlPlaneURL string) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
slugFromPath := strings.HasPrefix(r.URL.Path, "/s/")
slug := resolveSlug(r)
if slug == "" {
writeInvalidSlug(w)
Expand Down Expand Up @@ -965,16 +963,12 @@ func handleProxy(tunnels *sync.Map, controlPlaneURL string) http.HandlerFunc {
}
done := make(chan struct{})
flusher, _ := w.(http.Flusher)
setCookie := ""
if slugFromPath || r.URL.Query().Get("slug") != "" || extractSlugFromHost(r.Host) == slug {
setCookie = slug
}
respW := http.ResponseWriter(w)
if owner {
respW = &overlayInjectWriter{w: w, slug: slug}
}
tc.activeStreams.Add(1)
tc.streams.Store(streamID, &streamCtx{w: respW, done: done, flusher: flusher, setCookie: setCookie})
tc.streams.Store(streamID, &streamCtx{w: respW, done: done, flusher: flusher})
sendStreamEnd := func() {
f := make([]byte, 5)
f[0] = FrameStreamEnd
Expand Down
3 changes: 3 additions & 0 deletions packages/gateway/overlay.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@
return s ? new URL(s).origin : window.location.origin;
})();
function getSlug(){
var host = window.location.hostname;
var parts = host.split('.');
if (parts.length >= 3) return parts[0];
var s = new URLSearchParams(window.location.search).get('slug');
if (s) return s;
var m = window.location.pathname.match(/^\/s\/([^\/]+)/);
Expand Down
2 changes: 1 addition & 1 deletion website/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ export default function RootLayout({
{children}
{process.env.NODE_ENV === "development" && <Agentation />}
{/* <WormkeyOverlay slug="swift-dawn-84" />
<Script defer src="https://t.wormkey.run/.wormkey/overlay.js?slug=swift-dawn-84"></Script> */}
<Script defer src="https://wift-dawn-84.wormkey.run/.wormkey/overlay.js></Script> */}
<Analytics />
</ThemeProvider>
</body>
Expand Down