@@ -3,6 +3,7 @@ import fetch from 'node-fetch';
33import { StoredCredentials , UserInfo , TokenInfo , Team , AuthError , AuthenticationError } from '../../types/auth' ;
44import { MCPInstallationsResponse } from '../../types/mcp' ;
55import { buildAuthConfig } from '../../utils/auth-config' ;
6+ import { Device , DeviceInfo } from '../../utils/device-detection' ;
67
78export class DeployStackAPI {
89 private credentials : StoredCredentials ;
@@ -98,6 +99,162 @@ export class DeployStackAPI {
9899 return response ;
99100 }
100101
102+ /**
103+ * Get device by hardware ID
104+ * @param hardwareId Hardware fingerprint
105+ * @returns Device if found, null otherwise
106+ */
107+ async getDeviceByHardwareId ( hardwareId : string ) : Promise < Device | null > {
108+ try {
109+ const endpoint = `${ this . baseUrl } /api/users/me/devices` ;
110+ const response = await this . makeRequest ( endpoint ) ;
111+
112+ if ( response . success && response . devices ) {
113+ const device = response . devices . find ( ( d : Device ) => d . hardware_id === hardwareId ) ;
114+ return device || null ;
115+ }
116+
117+ return null ;
118+ } catch {
119+ // If we get a 404 or other error, assume no device found
120+ return null ;
121+ }
122+ }
123+
124+ /**
125+ * Create a new device
126+ * @param deviceInfo Device information
127+ * @returns Created device
128+ */
129+ async createDevice ( deviceInfo : DeviceInfo ) : Promise < Device > {
130+ const endpoint = `${ this . baseUrl } /api/users/me/devices` ;
131+ const deviceData = {
132+ device_name : deviceInfo . hostname , // Default to hostname
133+ hostname : deviceInfo . hostname ,
134+ hardware_id : deviceInfo . hardware_id ,
135+ os_type : deviceInfo . os_type ,
136+ os_version : deviceInfo . os_version ,
137+ arch : deviceInfo . arch ,
138+ node_version : deviceInfo . node_version ,
139+ user_agent : deviceInfo . user_agent ,
140+ last_login_at : new Date ( ) . toISOString ( ) ,
141+ last_activity_at : new Date ( ) . toISOString ( )
142+ } ;
143+
144+ const response = await this . makeRequest ( endpoint , {
145+ method : 'POST' ,
146+ body : JSON . stringify ( deviceData )
147+ } ) ;
148+
149+ if ( response . success && response . device ) {
150+ return response . device ;
151+ }
152+
153+ throw new AuthenticationError (
154+ AuthError . NETWORK_ERROR ,
155+ 'Failed to create device'
156+ ) ;
157+ }
158+
159+ /**
160+ * Update an existing device
161+ * @param deviceId Device ID
162+ * @param updates Device updates
163+ * @returns Updated device
164+ */
165+ async updateDevice ( deviceId : string , updates : { device_name ?: string } ) : Promise < Device > {
166+ const endpoint = `${ this . baseUrl } /api/users/me/devices/${ deviceId } ` ;
167+
168+ // The backend only accepts device_name updates via PUT
169+ const updateData = {
170+ device_name : updates . device_name || 'Updated Device'
171+ } ;
172+
173+ const response = await this . makeRequest ( endpoint , {
174+ method : 'PUT' ,
175+ body : JSON . stringify ( updateData )
176+ } ) ;
177+
178+ if ( response . success && response . device ) {
179+ return response . device ;
180+ }
181+
182+ throw new AuthenticationError (
183+ AuthError . NETWORK_ERROR ,
184+ 'Failed to update device'
185+ ) ;
186+ }
187+ /**
188+ * Update device activity (internal method)
189+ * This would be called during login to update last_login_at
190+ * For now, we'll just update the device name to trigger an update
191+ * @param deviceId Device ID
192+ * @returns Updated device
193+ */
194+ async updateDeviceActivity ( deviceId : string ) : Promise < Device > {
195+ // Since the backend only supports device_name updates,
196+ // we'll just update with the current name to trigger last update timestamp
197+ const endpoint = `${ this . baseUrl } /api/users/me/devices/${ deviceId } ` ;
198+
199+ // Get current device first to preserve the name
200+ const currentDevice = await this . getDeviceById ( deviceId ) ;
201+
202+ const updateData = {
203+ device_name : currentDevice . device_name
204+ } ;
205+
206+ const response = await this . makeRequest ( endpoint , {
207+ method : 'PUT' ,
208+ body : JSON . stringify ( updateData )
209+ } ) ;
210+
211+ if ( response . success && response . device ) {
212+ return response . device ;
213+ }
214+
215+ throw new AuthenticationError (
216+ AuthError . NETWORK_ERROR ,
217+ 'Failed to update device activity'
218+ ) ;
219+ }
220+
221+ /**
222+ * Get device by ID
223+ * @param deviceId Device ID
224+ * @returns Device
225+ */
226+ async getDeviceById ( deviceId : string ) : Promise < Device > {
227+ const endpoint = `${ this . baseUrl } /api/users/me/devices/${ deviceId } ` ;
228+ const response = await this . makeRequest ( endpoint ) ;
229+
230+ if ( response . success && response . device ) {
231+ return response . device ;
232+ }
233+
234+ throw new AuthenticationError (
235+ AuthError . NETWORK_ERROR ,
236+ 'Device not found'
237+ ) ;
238+ }
239+
240+ /**
241+ * Register or update a device (convenience method)
242+ * @param deviceInfo Device information
243+ * @returns Device (created or updated)
244+ */
245+ async registerOrUpdateDevice ( deviceInfo : DeviceInfo ) : Promise < Device > {
246+ // First, try to find existing device by hardware ID
247+ const existingDevice = await this . getDeviceByHardwareId ( deviceInfo . hardware_id ) ;
248+
249+ if ( existingDevice ) {
250+ // Update existing device activity (this will update the timestamp)
251+ return await this . updateDeviceActivity ( existingDevice . id ) ;
252+ } else {
253+ // Create new device
254+ return await this . createDevice ( deviceInfo ) ;
255+ }
256+ }
257+
101258 /**
102259 * Make an authenticated API request
103260 * @param endpoint API endpoint URL
0 commit comments