Skip to content

Commit c90640e

Browse files
author
Ryan Vogel
committed
feat: expose push pairing QR endpoints
Return both JSON metadata and a PNG QR so clients can consume mobile pairing without rebuilding the payload themselves.
1 parent 36b51ca commit c90640e

1 file changed

Lines changed: 107 additions & 0 deletions

File tree

packages/opencode/src/server/routes/experimental.ts

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,49 @@ import { errors } from "../error"
1313
import { lazy } from "../../util/lazy"
1414
import { WorkspaceRoutes } from "./workspace"
1515
import { PushRelay } from "../push-relay"
16+
import * as QRCode from "qrcode"
17+
18+
const PushPairPayload = z
19+
.object({
20+
v: z.literal(1),
21+
serverID: z.string().optional(),
22+
relayURL: z.string(),
23+
relaySecret: z.string(),
24+
hosts: z.array(z.string()),
25+
})
26+
.meta({ ref: "PushPairPayload" })
27+
28+
const PushPairResult = z
29+
.discriminatedUnion("enabled", [
30+
z.object({
31+
enabled: z.literal(false),
32+
}),
33+
z.object({
34+
enabled: z.literal(true),
35+
hosts: z.array(z.string()),
36+
qr: z.string(),
37+
}),
38+
])
39+
.meta({ ref: "PushPairResult" })
40+
41+
const pushPairQROptions = {
42+
errorCorrectionLevel: "M" as const,
43+
margin: 1,
44+
width: 256,
45+
}
46+
47+
function pushPairLink(payload: z.infer<typeof PushPairPayload>) {
48+
return `mobilevoice:///?pair=${encodeURIComponent(JSON.stringify(payload))}`
49+
}
50+
51+
async function pushPairQRCode(payload: z.infer<typeof PushPairPayload>) {
52+
return QRCode.toDataURL(pushPairLink(payload), pushPairQROptions)
53+
}
54+
55+
async function pushPairQRCodePNG(payload: z.infer<typeof PushPairPayload>) {
56+
const data = await pushPairQRCode(payload)
57+
return Buffer.from(data.replace(/^data:image\/png;base64,/, ""), "base64")
58+
}
1659

1760
export const ExperimentalRoutes = lazy(() =>
1861
new Hono()
@@ -269,6 +312,70 @@ export const ExperimentalRoutes = lazy(() =>
269312
return c.json(await MCP.resources())
270313
},
271314
)
315+
.get(
316+
"/push/pair",
317+
describeRoute({
318+
summary: "Get push relay pairing QR",
319+
description: "Get the active push relay pairing payload and QR code for mobile setup.",
320+
operationId: "experimental.push.pair",
321+
responses: {
322+
200: {
323+
description: "Push relay pairing info",
324+
content: {
325+
"application/json": {
326+
schema: resolver(PushPairResult),
327+
},
328+
},
329+
},
330+
},
331+
}),
332+
async (c) => {
333+
const pair = PushRelay.pair()
334+
if (!pair) {
335+
return c.json({
336+
enabled: false,
337+
})
338+
}
339+
340+
const qr = await pushPairQRCode(pair)
341+
342+
return c.json({
343+
enabled: true,
344+
hosts: pair.hosts,
345+
qr,
346+
})
347+
},
348+
)
349+
.get(
350+
"/push/pair/qr",
351+
describeRoute({
352+
summary: "Get push relay pairing QR image",
353+
description: "Render the active push relay pairing QR code as a PNG image.",
354+
operationId: "experimental.push.pair.qr",
355+
responses: {
356+
200: {
357+
description: "Push relay pairing QR image",
358+
content: {
359+
"image/png": {
360+
schema: { type: "string", format: "binary" } as any,
361+
},
362+
},
363+
},
364+
404: {
365+
description: "Push relay pairing is not enabled",
366+
},
367+
},
368+
}),
369+
async (c) => {
370+
const pair = PushRelay.pair()
371+
if (!pair) {
372+
return c.text("Push pairing is not enabled", 404)
373+
}
374+
375+
c.header("Content-Type", "image/png")
376+
return c.body(await pushPairQRCodePNG(pair))
377+
},
378+
)
272379
.get(
273380
"/push",
274381
describeRoute({

0 commit comments

Comments
 (0)