@@ -14,6 +14,97 @@ import {
1414import type { Env } from "./tunnel-do.js" ;
1515
1616const SESSION_COOKIE = "__Secure-better-auth.session_token" ;
17+ export const DESKTOP_SESSION_COOKIE = "__Secure-bb-connect.desktop_session" ;
18+ export const DESKTOP_SESSION_TTL_MS = 60 * 60 * 1000 ;
19+
20+ function bytesToBase64Url ( bytes : Uint8Array ) : string {
21+ return btoa ( String . fromCharCode ( ...bytes ) )
22+ . replace ( / \+ / g, "-" )
23+ . replace ( / \/ / g, "_" )
24+ . replace ( / = + $ / g, "" ) ;
25+ }
26+
27+ function stringToBase64Url ( value : string ) : string {
28+ return bytesToBase64Url ( new TextEncoder ( ) . encode ( value ) ) ;
29+ }
30+
31+ function base64UrlToString ( value : string ) : string | null {
32+ try {
33+ const base64 = value . replace ( / - / g, "+" ) . replace ( / _ / g, "/" ) ;
34+ const padded = base64 . padEnd ( Math . ceil ( base64 . length / 4 ) * 4 , "=" ) ;
35+ return new TextDecoder ( ) . decode (
36+ Uint8Array . from ( atob ( padded ) , ( character ) => character . charCodeAt ( 0 ) ) ,
37+ ) ;
38+ } catch {
39+ return null ;
40+ }
41+ }
42+
43+ async function signDesktopSessionPayload (
44+ payload : string ,
45+ secret : string ,
46+ ) : Promise < string > {
47+ const key = await crypto . subtle . importKey (
48+ "raw" ,
49+ new TextEncoder ( ) . encode ( secret ) ,
50+ { name : "HMAC" , hash : "SHA-256" } ,
51+ false ,
52+ [ "sign" ] ,
53+ ) ;
54+ const signature = await crypto . subtle . sign (
55+ "HMAC" ,
56+ key ,
57+ new TextEncoder ( ) . encode ( payload ) ,
58+ ) ;
59+ return bytesToBase64Url ( new Uint8Array ( signature ) ) ;
60+ }
61+
62+ export async function createDesktopSessionCookie (
63+ userId : string ,
64+ secret : string ,
65+ expiresAt : number ,
66+ ) : Promise < string > {
67+ const payload = stringToBase64Url ( JSON . stringify ( { expiresAt, userId } ) ) ;
68+ return `${ payload } .${ await signDesktopSessionPayload ( payload , secret ) } ` ;
69+ }
70+
71+ export async function verifyDesktopSessionCookie (
72+ cookieValue : string ,
73+ secret : string ,
74+ now : number = Date . now ( ) ,
75+ ) : Promise < string | null > {
76+ const dot = cookieValue . lastIndexOf ( "." ) ;
77+ if ( dot <= 0 ) return null ;
78+ const payload = cookieValue . slice ( 0 , dot ) ;
79+ const signature = cookieValue . slice ( dot + 1 ) ;
80+ const expected = await signDesktopSessionPayload ( payload , secret ) ;
81+ if ( signature . length !== expected . length ) return null ;
82+ let mismatch = 0 ;
83+ for ( let index = 0 ; index < signature . length ; index += 1 ) {
84+ mismatch |= signature . charCodeAt ( index ) ^ expected . charCodeAt ( index ) ;
85+ }
86+ if ( mismatch !== 0 ) return null ;
87+
88+ const decoded = base64UrlToString ( payload ) ;
89+ if ( decoded === null ) return null ;
90+ try {
91+ const value : unknown = JSON . parse ( decoded ) ;
92+ if (
93+ typeof value !== "object" ||
94+ value === null ||
95+ ! ( "userId" in value ) ||
96+ typeof value . userId !== "string" ||
97+ ! ( "expiresAt" in value ) ||
98+ typeof value . expiresAt !== "number" ||
99+ value . expiresAt <= now
100+ ) {
101+ return null ;
102+ }
103+ return value . userId ;
104+ } catch {
105+ return null ;
106+ }
107+ }
17108
18109const serverCredentialCache = new Map <
19110 string ,
@@ -22,8 +113,13 @@ const serverCredentialCache = new Map<
22113const SERVER_CRED_TTL_MS = 20_000 ;
23114
24115async function sha256Hex ( value : string ) : Promise < string > {
25- const digest = await crypto . subtle . digest ( "SHA-256" , new TextEncoder ( ) . encode ( value ) ) ;
26- return [ ...new Uint8Array ( digest ) ] . map ( ( b ) => b . toString ( 16 ) . padStart ( 2 , "0" ) ) . join ( "" ) ;
116+ const digest = await crypto . subtle . digest (
117+ "SHA-256" ,
118+ new TextEncoder ( ) . encode ( value ) ,
119+ ) ;
120+ return [ ...new Uint8Array ( digest ) ]
121+ . map ( ( b ) => b . toString ( 16 ) . padStart ( 2 , "0" ) )
122+ . join ( "" ) ;
27123}
28124
29125/**
@@ -145,12 +241,19 @@ export async function handleListAccountServers(
145241 if ( request . method !== "GET" ) {
146242 return new Response ( JSON . stringify ( { error : "method_not_allowed" } ) , {
147243 status : 405 ,
148- headers : { "content-type" : "application/json; charset=utf-8" , allow : "GET" } ,
244+ headers : {
245+ "content-type" : "application/json; charset=utf-8" ,
246+ allow : "GET" ,
247+ } ,
149248 } ) ;
150249 }
151250
152251 const db = drizzle ( env . DB , { schema } ) ;
153- const userId = await resolveAccountUserId ( request , env . BETTER_AUTH_SECRET , db ) ;
252+ const userId = await resolveAccountUserId (
253+ request ,
254+ env . BETTER_AUTH_SECRET ,
255+ db ,
256+ ) ;
154257 if ( ! userId ) {
155258 return new Response ( JSON . stringify ( { error : "unauthorized" } ) , {
156259 status : 401 ,
@@ -164,3 +267,51 @@ export async function handleListAccountServers(
164267 headers : { "content-type" : "application/json; charset=utf-8" } ,
165268 } ) ;
166269}
270+
271+ /** Exchange a durable pairing credential for a short-lived browser session. */
272+ export async function handleCreateDesktopSession (
273+ request : Request ,
274+ env : Env ,
275+ ) : Promise < Response > {
276+ if ( request . method !== "POST" ) {
277+ return new Response ( JSON . stringify ( { error : "method_not_allowed" } ) , {
278+ status : 405 ,
279+ headers : {
280+ "content-type" : "application/json; charset=utf-8" ,
281+ allow : "POST" ,
282+ } ,
283+ } ) ;
284+ }
285+ const db = drizzle ( env . DB , { schema } ) ;
286+ const userId = await resolveAccountUserId (
287+ request ,
288+ env . BETTER_AUTH_SECRET ,
289+ db ,
290+ ) ;
291+ if ( ! userId ) {
292+ return new Response ( JSON . stringify ( { error : "unauthorized" } ) , {
293+ status : 401 ,
294+ headers : { "content-type" : "application/json; charset=utf-8" } ,
295+ } ) ;
296+ }
297+ const expiresAt = Date . now ( ) + DESKTOP_SESSION_TTL_MS ;
298+ const value = await createDesktopSessionCookie (
299+ userId ,
300+ env . BETTER_AUTH_SECRET ,
301+ expiresAt ,
302+ ) ;
303+ return new Response (
304+ JSON . stringify ( {
305+ cookie : {
306+ domain : `.${ env . BASE_DOMAIN } ` ,
307+ expiresAt,
308+ name : DESKTOP_SESSION_COOKIE ,
309+ value,
310+ } ,
311+ } ) ,
312+ {
313+ status : 200 ,
314+ headers : { "content-type" : "application/json; charset=utf-8" } ,
315+ } ,
316+ ) ;
317+ }
0 commit comments