1+ import { z } from "zod" ;
2+
3+ import { DeploymentSchema , type Deployment } from "../deployment/types" ;
14import { type Logger } from "../logging/logger" ;
25import { type OAuth2ClientRegistrationResponse } from "../oauth/types" ;
36import { toSafeHost } from "../util" ;
47
58import type { Memento , SecretStorage , Disposable } from "vscode" ;
69
7- import type { Deployment } from "../deployment/types" ;
8-
910// Each deployment has its own key to ensure atomic operations (multiple windows
1011// writing to a shared key could drop data) and to receive proper VS Code events.
1112const SESSION_KEY_PREFIX = "coder.session." ;
@@ -22,39 +23,50 @@ const DEFAULT_MAX_DEPLOYMENTS = 10;
2223
2324const LEGACY_SESSION_TOKEN_KEY = "sessionToken" ;
2425
25- export interface CurrentDeploymentState {
26- deployment : Deployment | null ;
27- }
26+ const CurrentDeploymentStateSchema = z . object ( {
27+ deployment : DeploymentSchema . nullable ( ) ,
28+ } ) ;
29+
30+ export type CurrentDeploymentState = z . infer <
31+ typeof CurrentDeploymentStateSchema
32+ > ;
2833
2934/**
3035 * OAuth token data stored alongside session auth.
3136 * When present, indicates the session is authenticated via OAuth.
3237 */
33- export interface OAuthTokenData {
34- token_type : "Bearer" ;
35- refresh_token ?: string ;
36- scope ?: string ;
37- expiry_timestamp : number ;
38- }
38+ const OAuthTokenDataSchema = z . object ( {
39+ refresh_token : z . string ( ) . optional ( ) ,
40+ scope : z . string ( ) . optional ( ) ,
41+ expiry_timestamp : z . number ( ) ,
42+ } ) ;
43+
44+ export type OAuthTokenData = z . infer < typeof OAuthTokenDataSchema > ;
3945
40- export interface SessionAuth {
41- url : string ;
42- token : string ;
46+ const SessionAuthSchema = z . object ( {
47+ url : z . string ( ) ,
48+ token : z . string ( ) ,
4349 /** If present, this session uses OAuth authentication */
44- oauth ?: OAuthTokenData ;
45- }
50+ oauth : OAuthTokenDataSchema . optional ( ) ,
51+ } ) ;
52+
53+ export type SessionAuth = z . infer < typeof SessionAuthSchema > ;
4654
4755// Tracks when a deployment was last accessed for LRU pruning.
48- interface DeploymentUsage {
49- safeHostname : string ;
50- lastAccessedAt : string ;
51- }
56+ const DeploymentUsageSchema = z . object ( {
57+ safeHostname : z . string ( ) ,
58+ lastAccessedAt : z . string ( ) ,
59+ } ) ;
5260
53- interface OAuthCallbackData {
54- state : string ;
55- code : string | null ;
56- error : string | null ;
57- }
61+ type DeploymentUsage = z . infer < typeof DeploymentUsageSchema > ;
62+
63+ const OAuthCallbackDataSchema = z . object ( {
64+ state : z . string ( ) ,
65+ code : z . string ( ) . nullable ( ) ,
66+ error : z . string ( ) . nullable ( ) ,
67+ } ) ;
68+
69+ type OAuthCallbackData = z . infer < typeof OAuthCallbackDataSchema > ;
5870
5971export class SecretsManager {
6072 constructor (
@@ -107,17 +119,18 @@ export class SecretsManager {
107119 public async setCurrentDeployment (
108120 deployment : Deployment | undefined ,
109121 ) : Promise < void > {
110- const state : CurrentDeploymentState & { timestamp : string } = {
111- // Extract the necessary fields before serializing
112- deployment : deployment
113- ? {
114- url : deployment ?. url ,
115- safeHostname : deployment ?. safeHostname ,
116- }
117- : null ,
122+ const state = CurrentDeploymentStateSchema . parse ( {
123+ deployment : deployment ?? null ,
124+ } ) ;
125+ // Add timestamp for cross-window change detection
126+ const stateWithTimestamp = {
127+ ...state ,
118128 timestamp : new Date ( ) . toISOString ( ) ,
119129 } ;
120- await this . secrets . store ( CURRENT_DEPLOYMENT_KEY , JSON . stringify ( state ) ) ;
130+ await this . secrets . store (
131+ CURRENT_DEPLOYMENT_KEY ,
132+ JSON . stringify ( stateWithTimestamp ) ,
133+ ) ;
121134 }
122135
123136 /**
@@ -129,8 +142,9 @@ export class SecretsManager {
129142 if ( ! data ) {
130143 return null ;
131144 }
132- const parsed = JSON . parse ( data ) as CurrentDeploymentState ;
133- return parsed . deployment ;
145+ const parsed : unknown = JSON . parse ( data ) ;
146+ const result = CurrentDeploymentStateSchema . safeParse ( parsed ) ;
147+ return result . success ? result . data . deployment : null ;
134148 } catch {
135149 return null ;
136150 }
@@ -181,22 +195,26 @@ export class SecretsManager {
181195 } ) ;
182196 }
183197
184- public getSessionAuth (
198+ public async getSessionAuth (
185199 safeHostname : string ,
186200 ) : Promise < SessionAuth | undefined > {
187- return this . getSecret < SessionAuth > ( SESSION_KEY_PREFIX , safeHostname ) ;
201+ const data = await this . getSecret < unknown > (
202+ SESSION_KEY_PREFIX ,
203+ safeHostname ,
204+ ) ;
205+ if ( ! data ) {
206+ return undefined ;
207+ }
208+ const result = SessionAuthSchema . safeParse ( data ) ;
209+ return result . success ? result . data : undefined ;
188210 }
189211
190212 public async setSessionAuth (
191213 safeHostname : string ,
192214 auth : SessionAuth ,
193215 ) : Promise < void > {
194- // Extract relevant fields before serializing
195- const state : SessionAuth = {
196- url : auth . url ,
197- token : auth . token ,
198- ...( auth . oauth && { oauth : auth . oauth } ) ,
199- } ;
216+ // Parse through schema to strip any extra fields
217+ const state = SessionAuthSchema . parse ( auth ) ;
200218 await this . setSecret ( SESSION_KEY_PREFIX , safeHostname , state ) ;
201219 }
202220
@@ -214,10 +232,11 @@ export class SecretsManager {
214232 ) : Promise < void > {
215233 const usage = this . getDeploymentUsage ( ) ;
216234 const filtered = usage . filter ( ( u ) => u . safeHostname !== safeHostname ) ;
217- filtered . unshift ( {
235+ const newEntry = DeploymentUsageSchema . parse ( {
218236 safeHostname,
219237 lastAccessedAt : new Date ( ) . toISOString ( ) ,
220238 } ) ;
239+ filtered . unshift ( newEntry ) ;
221240
222241 const toKeep = filtered . slice ( 0 , maxCount ) ;
223242 const toRemove = filtered . slice ( maxCount ) ;
@@ -253,7 +272,12 @@ export class SecretsManager {
253272 * Get the full deployment usage list with access timestamps.
254273 */
255274 private getDeploymentUsage ( ) : DeploymentUsage [ ] {
256- return this . memento . get < DeploymentUsage [ ] > ( DEPLOYMENT_USAGE_KEY ) ?? [ ] ;
275+ const data = this . memento . get < unknown > ( DEPLOYMENT_USAGE_KEY ) ;
276+ if ( ! data ) {
277+ return [ ] ;
278+ }
279+ const result = z . array ( DeploymentUsageSchema ) . safeParse ( data ) ;
280+ return result . success ? result . data : [ ] ;
257281 }
258282
259283 /**
@@ -294,7 +318,8 @@ export class SecretsManager {
294318 * Used for cross-window communication when OAuth callback arrives in a different window.
295319 */
296320 public async setOAuthCallback ( data : OAuthCallbackData ) : Promise < void > {
297- await this . secrets . store ( OAUTH_CALLBACK_KEY , JSON . stringify ( data ) ) ;
321+ const parsed = OAuthCallbackDataSchema . parse ( data ) ;
322+ await this . secrets . store ( OAUTH_CALLBACK_KEY , JSON . stringify ( parsed ) ) ;
298323 }
299324
300325 /**
@@ -309,20 +334,27 @@ export class SecretsManager {
309334 return ;
310335 }
311336
312- let parsed : OAuthCallbackData ;
337+ const raw = await this . secrets . get ( OAUTH_CALLBACK_KEY ) ;
338+ if ( ! raw ) {
339+ return ;
340+ }
341+
342+ let parsed : unknown ;
313343 try {
314- const data = await this . secrets . get ( OAUTH_CALLBACK_KEY ) ;
315- if ( ! data ) {
316- return ;
317- }
318- parsed = JSON . parse ( data ) as OAuthCallbackData ;
344+ parsed = JSON . parse ( raw ) ;
319345 } catch ( err ) {
320- this . logger . error ( "Failed to parse OAuth callback data" , err ) ;
346+ this . logger . error ( "Failed to parse OAuth callback JSON" , err ) ;
347+ return ;
348+ }
349+
350+ const result = OAuthCallbackDataSchema . safeParse ( parsed ) ;
351+ if ( ! result . success ) {
352+ this . logger . error ( "Invalid OAuth callback data shape" , result . error ) ;
321353 return ;
322354 }
323355
324356 try {
325- listener ( parsed ) ;
357+ listener ( result . data ) ;
326358 } catch ( err ) {
327359 this . logger . error ( "Error in onDidChangeOAuthCallback listener" , err ) ;
328360 }
0 commit comments