feat: persist APN relay secret across restarts, add dev logging and server identification to pair UI

- Electron desktop: auto-start PushRelay with secret persisted in electron-store
- CLI serve (Tauri): persist relay secret to Global.Path.state/relay-secret (mode 0600)
- Pair endpoint now returns relayURL, serverID, relaySecretHash for debugging
- Desktop settings-pair component shows server name, relay URL, and secret hash above QR
- Add console.debug logging for pairing fetch lifecycle
- Export PushRelay from node.ts entry point for Electron consumption
This commit is contained in:
Ryan Vogel
2026-04-17 22:31:02 +00:00
parent 38d4d03ba8
commit 754951bbbd
7 changed files with 154 additions and 28 deletions
+19 -9
View File
@@ -1,5 +1,7 @@
import { spawnSync } from "node:child_process"
import { createHash, randomBytes } from "node:crypto"
import { writeFileSync } from "node:fs"
import path from "node:path"
import os from "node:os"
import { Server } from "../../server/server"
import { cmd } from "./cmd"
@@ -10,10 +12,24 @@ import { Project } from "../../project"
import { Installation } from "../../installation"
import { PushRelay } from "../../server/push-relay"
import { Log } from "../../util"
import { Global } from "../../global"
import * as QRCode from "qrcode"
const log = Log.create({ service: "serve" })
async function getOrCreatePersistedRelaySecret(): Promise<string> {
const filePath = path.join(Global.Path.state, "relay-secret")
try {
const existing = (await Bun.file(filePath).text()).trim()
if (existing.length > 0) return existing
} catch {
// file doesn't exist yet
}
const secret = randomBytes(18).toString("base64url")
writeFileSync(filePath, secret, { mode: 0o600 })
return secret
}
type PairPayload = {
serverID?: string
relayURL: string
@@ -225,7 +241,7 @@ export const ServeCommand = cmd({
]
const input = (args["relay-secret"] ?? process.env.OPENCODE_EXPERIMENTAL_PUSH_RELAY_SECRET ?? "").trim()
const relaySecret = input || randomBytes(18).toString("base64url")
const relaySecret = input || (await getOrCreatePersistedRelaySecret())
const connectQR = Boolean(args["connect-qr"])
if (connectQR) {
@@ -236,10 +252,7 @@ export const ServeCommand = cmd({
}
if (!input) {
console.log("experimental push relay secret generated")
console.log(
"set --relay-secret or OPENCODE_EXPERIMENTAL_PUSH_RELAY_SECRET to keep push registrations stable across server restarts",
)
log.info("using persisted relay secret", { hash: secretHash(relaySecret) })
}
console.log("printing connect qr without starting the server")
@@ -259,10 +272,7 @@ export const ServeCommand = cmd({
console.log(`opencode server listening on http://${server.hostname}:${server.port}`)
if (!input) {
console.log("experimental push relay secret generated")
console.log(
"set --relay-secret or OPENCODE_EXPERIMENTAL_PUSH_RELAY_SECRET to keep push registrations stable across server restarts",
)
log.info("using persisted relay secret", { hash: secretHash(relaySecret) })
}
if (relayURL && relaySecret) {
const host = server.hostname ?? opts.hostname
+1
View File
@@ -1,5 +1,6 @@
export { Config } from "./config"
export { Server } from "./server/server"
export { PushRelay } from "./server/push-relay"
export { bootstrap } from "./cli/bootstrap"
export { Log } from "./util"
export { Database } from "./storage"
@@ -1,3 +1,4 @@
import { createHash } from "node:crypto"
import { Hono } from "hono"
import { describeRoute, validator, resolver } from "hono-openapi"
import z from "zod"
@@ -36,6 +37,9 @@ const PushPairResult = z
z.object({
enabled: z.literal(true),
hosts: z.array(z.string()),
relayURL: z.string(),
serverID: z.string().optional(),
relaySecretHash: z.string(),
link: z.string(),
qr: z.string(),
}),
@@ -488,10 +492,16 @@ export const ExperimentalRoutes = lazy(() =>
const link = pushPairLink(pair)
const qr = await pushPairQRCode(pair)
const relaySecretHash = pair.relaySecret
? `${createHash("sha256").update(pair.relaySecret).digest("hex").slice(0, 12)}...`
: "none"
return c.json({
enabled: true,
hosts: pair.hosts,
relayURL: pair.relayURL,
serverID: pair.serverID,
relaySecretHash,
link,
qr,
})