Skip to content

Commit 34409cf

Browse files
committed
feat: default control plane and API_TOKEN tunnel auth
Default SPAWNDOCK_CONTROL_PLANE to production when unset; allow WebSocket auth via API_TOKEN query param alongside legacy device secret. Made-with: Cursor
1 parent b490f95 commit 34409cf

6 files changed

Lines changed: 47 additions & 9 deletions

File tree

README.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,11 +39,14 @@ npx @spawn-dock/dev-tunnel \
3939
```bash
4040
export SPAWNDOCK_CONTROL_PLANE=http://your-server:3000
4141
export SPAWNDOCK_PROJECT_SLUG=my-app
42-
export SPAWNDOCK_DEVICE_SECRET=your-device-secret
42+
export API_TOKEN=your-shared-api-token
4343
export SPAWNDOCK_PORT=3000
4444
npx @spawn-dock/dev-tunnel
4545
```
4646

47+
`API_TOKEN` is the preferred auth mode for standalone package usage.
48+
`SPAWNDOCK_DEVICE_SECRET` remains supported for bootstrap-generated project credentials.
49+
4750
## Configuration Priority
4851

4952
CLI arguments > Environment variables > `spawndock.dev-tunnel.json` > legacy `spawndock.config.json`

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/__tests__/config.test.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,14 @@ describe("resolveConfig", () => {
2626
expect(() => resolveConfig([])).toThrow("Missing");
2727
});
2828

29+
it("allows auth via API_TOKEN without device secret", () => {
30+
vi.stubEnv("SPAWNDOCK_PROJECT_SLUG", "my-app");
31+
vi.stubEnv("API_TOKEN", "shared_token");
32+
const config = resolveConfig([]);
33+
expect(config.controlPlane).toBe("https://spawn-dock.w3voice.net");
34+
expect(config.apiToken).toBe("shared_token");
35+
});
36+
2937
it("defaults port to 3000", () => {
3038
const config = resolveConfig([
3139
"--control-plane", "http://localhost:8787",

src/__tests__/tunnel.test.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,17 @@ describe("buildWsUrl", () => {
1515
"wss://api.example.com/tunnel/connect?protocolVersion=1&token=secret123",
1616
);
1717
});
18+
19+
it("includes apiToken query param when API_TOKEN is used", () => {
20+
const url = buildWsUrl({
21+
controlPlane: "https://api.example.com",
22+
projectSlug: "demo",
23+
apiToken: "shared_token",
24+
port: 3000,
25+
});
26+
27+
expect(url).toBe(
28+
"wss://api.example.com/tunnel/connect?protocolVersion=1&apiToken=shared_token",
29+
);
30+
});
1831
});

src/config.ts

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ import { resolve } from "node:path";
44
export interface TunnelConfig {
55
controlPlane: string;
66
projectSlug: string;
7-
deviceSecret: string;
7+
deviceSecret?: string;
8+
apiToken?: string;
89
port: number;
910
previewPath?: string;
1011
telegramMiniAppUrl?: string;
@@ -42,6 +43,10 @@ function normalizeConfig(data: unknown): Partial<TunnelConfig> {
4243
: typeof record.deviceToken === "string"
4344
? record.deviceToken
4445
: undefined;
46+
const apiToken =
47+
typeof record.apiToken === "string"
48+
? record.apiToken
49+
: undefined;
4550
const port =
4651
typeof record.port === "number"
4752
? record.port
@@ -53,7 +58,7 @@ function normalizeConfig(data: unknown): Partial<TunnelConfig> {
5358
const telegramMiniAppUrl =
5459
typeof record.telegramMiniAppUrl === "string" ? record.telegramMiniAppUrl : undefined;
5560

56-
return { controlPlane, projectSlug, deviceSecret, port, previewPath, telegramMiniAppUrl };
61+
return { controlPlane, projectSlug, deviceSecret, apiToken, port, previewPath, telegramMiniAppUrl };
5762
}
5863

5964
function readConfigFile(dir: string): Partial<TunnelConfig> {
@@ -139,20 +144,24 @@ export function resolveConfig(
139144
controlPlane: process.env.SPAWNDOCK_CONTROL_PLANE,
140145
projectSlug: process.env.SPAWNDOCK_PROJECT_SLUG,
141146
deviceSecret: process.env.SPAWNDOCK_DEVICE_SECRET,
147+
apiToken: process.env.API_TOKEN || process.env.SPAWNDOCK_API_TOKEN,
142148
port: readNumber(process.env.SPAWNDOCK_PORT),
143149
};
144150

145151
// Priority: CLI > Env > File
146-
const controlPlane = args.controlPlane ?? env.controlPlane ?? file.controlPlane;
152+
const controlPlane = args.controlPlane ?? env.controlPlane ?? file.controlPlane ?? "https://spawn-dock.w3voice.net";
147153
const projectSlug = args.projectSlug ?? env.projectSlug ?? file.projectSlug;
148154
const deviceSecret = args.deviceSecret ?? env.deviceSecret ?? file.deviceSecret;
155+
const apiToken = env.apiToken ?? file.apiToken;
149156
const port = args.port ?? env.port ?? file.port ?? 3000;
150157
const previewPath = file.previewPath;
151158
const telegramMiniAppUrl = file.telegramMiniAppUrl;
152159

153160
if (!controlPlane) throw new Error("Missing --control-plane or SPAWNDOCK_CONTROL_PLANE");
154161
if (!projectSlug) throw new Error("Missing --project-slug or SPAWNDOCK_PROJECT_SLUG");
155-
if (!deviceSecret) throw new Error("Missing --device-secret or SPAWNDOCK_DEVICE_SECRET");
162+
if (!deviceSecret && !apiToken) {
163+
throw new Error("Missing authentication: set API_TOKEN (preferred) or --device-secret");
164+
}
156165

157-
return { controlPlane, projectSlug, deviceSecret, port, previewPath, telegramMiniAppUrl };
166+
return { controlPlane, projectSlug, deviceSecret, apiToken, port, previewPath, telegramMiniAppUrl };
158167
}

src/tunnel.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,12 @@ export function buildWsUrl(config: TunnelConfig): string {
4545
const currentPath = url.pathname.replace(/\/+$/, "");
4646
url.pathname = currentPath.length === 0 ? "/tunnel/connect" : `${currentPath}/tunnel/connect`;
4747
url.searchParams.set("protocolVersion", "1");
48-
url.searchParams.set("token", config.deviceSecret);
48+
if (config.deviceSecret && config.deviceSecret.length > 0) {
49+
url.searchParams.set("token", config.deviceSecret);
50+
}
51+
if (config.apiToken && config.apiToken.length > 0) {
52+
url.searchParams.set("apiToken", config.apiToken);
53+
}
4954
return url.toString();
5055
}
5156

0 commit comments

Comments
 (0)