@@ -20,9 +20,20 @@ import { LegacyInvalidAccessTokenError } from "./legacy-errors.ts";
2020
2121const passwords = new Map < string , string > ( ) ;
2222let throwOnSetPassword = false ;
23+ let throwOnSetSecret = false ;
2324const throwOnGetPasswordAccounts = new Set < string > ( ) ;
25+ const withTargetCalls : string [ ] = [ ] ;
2426
2527vi . mock ( "@napi-rs/keyring" , ( ) => ( {
28+ findCredentials : ( service : string , target ?: string ) =>
29+ Array . from ( passwords . entries ( ) )
30+ . filter ( ( [ key ] ) =>
31+ target === undefined ? key . startsWith ( `${ service } /` ) : key . startsWith ( `${ target } /` ) ,
32+ )
33+ . map ( ( [ key , password ] ) => ( {
34+ account : key . split ( "/" ) . at ( - 1 ) ! ,
35+ password,
36+ } ) ) ,
2637 Entry : class Entry {
2738 service : string ;
2839 account : string ;
@@ -33,6 +44,7 @@ vi.mock("@napi-rs/keyring", () => ({
3344 this . target = target ;
3445 }
3546 static withTarget ( target : string , service : string , account : string ) {
47+ withTargetCalls . push ( `${ target } /${ service } /${ account } ` ) ;
3648 return new this ( service , account , target ) ;
3749 }
3850 key ( ) : string {
@@ -51,6 +63,10 @@ vi.mock("@napi-rs/keyring", () => ({
5163 if ( throwOnSetPassword ) throw new Error ( "Keyring unavailable" ) ;
5264 passwords . set ( this . key ( ) , value ) ;
5365 }
66+ setSecret ( value : Uint8Array ) : void {
67+ if ( throwOnSetSecret ) throw new Error ( "Keyring unavailable" ) ;
68+ passwords . set ( this . key ( ) , Buffer . from ( value ) . toString ( "utf8" ) ) ;
69+ }
5470 deleteCredential ( ) : boolean {
5571 const key = this . key ( ) ;
5672 if ( ! passwords . has ( key ) ) throw new Error ( "not found" ) ;
@@ -66,10 +82,20 @@ vi.mock("@napi-rs/keyring", () => ({
6682
6783let tempHome : string ;
6884
69- function makeLayer ( opts : { env ?: Record < string , string | undefined > ; home ?: string } = { } ) {
85+ function makeLayer (
86+ opts : {
87+ env ?: Record < string , string | undefined > ;
88+ home ?: string ;
89+ platform ?: NodeJS . Platform ;
90+ } = { } ,
91+ ) {
7092 const home = opts . home ?? tempHome ;
7193 const env = { HOME : home , ...opts . env } ;
72- const runtimeInfoLayer = mockRuntimeInfo ( { homeDir : home , cwd : home } ) ;
94+ const runtimeInfoLayer = mockRuntimeInfo ( {
95+ homeDir : home ,
96+ cwd : home ,
97+ platform : opts . platform ,
98+ } ) ;
7399 const cliConfigLayer = legacyCliConfigLayer . pipe (
74100 Layer . provide ( Layer . succeed ( LegacyProfileFlag , "supabase" ) ) ,
75101 Layer . provide ( Layer . succeed ( LegacyWorkdirFlag , Option . none < string > ( ) ) ) ,
@@ -88,7 +114,9 @@ function makeLayer(opts: { env?: Record<string, string | undefined>; home?: stri
88114beforeEach ( ( ) => {
89115 passwords . clear ( ) ;
90116 throwOnSetPassword = false ;
117+ throwOnSetSecret = false ;
91118 throwOnGetPasswordAccounts . clear ( ) ;
119+ withTargetCalls . length = 0 ;
92120 tempHome = mkdtempSync ( join ( tmpdir ( ) , "supabase-legacy-creds-" ) ) ;
93121} ) ;
94122
@@ -101,6 +129,14 @@ const VALID_OAUTH_TOKEN = "sbp_oauth_" + "b".repeat(40);
101129const encodeGoKeyringBase64 = ( token : string ) =>
102130 `go-keyring-base64:${ Buffer . from ( token ) . toString ( "base64" ) } ` ;
103131const goWindowsKey = ( account : string ) => `Supabase CLI:${ account } /Supabase CLI/${ account } ` ;
132+ const encodeGoWindowsPassword = ( token : string ) => {
133+ const bytes = Buffer . from ( token , "utf8" ) ;
134+ let encoded = "" ;
135+ for ( let index = 0 ; index < bytes . length ; index += 2 ) {
136+ encoded += String . fromCharCode ( bytes [ index ] ! | ( ( bytes [ index + 1 ] ?? 0 ) << 8 ) ) ;
137+ }
138+ return encoded ;
139+ } ;
104140
105141const expectSomeToken = ( token : Option . Option < Redacted . Redacted < string > > , expected : string ) => {
106142 expect ( Option . isSome ( token ) ) . toBe ( true ) ;
@@ -138,12 +174,23 @@ describe("legacyCredentialsLayer.getAccessToken", () => {
138174 } ) ;
139175
140176 it . effect ( "reads Windows credentials created by Go keyring" , ( ) => {
141- passwords . set ( goWindowsKey ( "supabase" ) , VALID_TOKEN ) ;
177+ passwords . set ( goWindowsKey ( "supabase" ) , encodeGoWindowsPassword ( VALID_TOKEN ) ) ;
142178 return Effect . gen ( function * ( ) {
143179 const { getAccessToken } = yield * LegacyCredentials ;
144180 const token = yield * getAccessToken ;
145181 expectSomeToken ( token , VALID_TOKEN ) ;
146- } ) . pipe ( Effect . provide ( makeLayer ( ) ) ) ;
182+ expect ( withTargetCalls ) . toEqual ( [ ] ) ;
183+ } ) . pipe ( Effect . provide ( makeLayer ( { platform : "win32" } ) ) ) ;
184+ } ) ;
185+
186+ it . effect ( "does not search Go Windows targets on other platforms" , ( ) => {
187+ passwords . set ( goWindowsKey ( "supabase" ) , VALID_TOKEN ) ;
188+ return Effect . gen ( function * ( ) {
189+ const { getAccessToken } = yield * LegacyCredentials ;
190+ const token = yield * getAccessToken ;
191+ expect ( token ) . toEqual ( Option . none ( ) ) ;
192+ expect ( withTargetCalls ) . toEqual ( [ ] ) ;
193+ } ) . pipe ( Effect . provide ( makeLayer ( { platform : "linux" } ) ) ) ;
147194 } ) ;
148195
149196 it . effect ( "falls through to the legacy access-token keyring entry" , ( ) => {
@@ -222,6 +269,27 @@ describe("legacyCredentialsLayer.saveAccessToken", () => {
222269 } ) . pipe ( Effect . provide ( makeLayer ( ) ) ) ,
223270 ) ;
224271
272+ it . effect ( "writes Windows credentials where Go keyring reads them" , ( ) =>
273+ Effect . gen ( function * ( ) {
274+ const { saveAccessToken } = yield * LegacyCredentials ;
275+ yield * saveAccessToken ( VALID_TOKEN ) ;
276+ expect ( passwords . get ( goWindowsKey ( "supabase" ) ) ) . toBe ( VALID_TOKEN ) ;
277+ expect ( passwords . has ( "Supabase CLI/supabase" ) ) . toBe ( false ) ;
278+ } ) . pipe ( Effect . provide ( makeLayer ( { platform : "win32" } ) ) ) ,
279+ ) ;
280+
281+ it . effect ( "falls back to the shared token file when Windows target writes fail" , ( ) => {
282+ throwOnSetSecret = true ;
283+ return Effect . gen ( function * ( ) {
284+ const { saveAccessToken } = yield * LegacyCredentials ;
285+ yield * saveAccessToken ( VALID_TOKEN ) ;
286+ expect ( passwords . has ( goWindowsKey ( "supabase" ) ) ) . toBe ( false ) ;
287+ expect ( passwords . has ( "Supabase CLI/supabase" ) ) . toBe ( false ) ;
288+ const content = readFileSync ( join ( tempHome , ".supabase" , "access-token" ) , "utf-8" ) ;
289+ expect ( content ) . toBe ( VALID_TOKEN ) ;
290+ } ) . pipe ( Effect . provide ( makeLayer ( { platform : "win32" } ) ) ) ;
291+ } ) ;
292+
225293 it . effect ( "falls back to the filesystem when the keyring write throws" , ( ) => {
226294 throwOnSetPassword = true ;
227295 return Effect . gen ( function * ( ) {
@@ -255,7 +323,7 @@ describe("legacyCredentialsLayer.deleteAccessToken", () => {
255323 expect ( passwords . has ( "Supabase CLI/access-token" ) ) . toBe ( false ) ;
256324 expect ( passwords . has ( goWindowsKey ( "supabase" ) ) ) . toBe ( false ) ;
257325 expect ( existsSync ( join ( supaDir , "access-token" ) ) ) . toBe ( false ) ;
258- } ) . pipe ( Effect . provide ( makeLayer ( ) ) ) ;
326+ } ) . pipe ( Effect . provide ( makeLayer ( { platform : "win32" } ) ) ) ;
259327 } ) ;
260328} ) ;
261329
0 commit comments