File size: 9,374 Bytes
e49e31a
b4691b7
0e86155
e49e31a
b4691b7
0e86155
 
e49e31a
0e86155
 
 
 
 
 
 
 
 
 
 
 
e49e31a
0e86155
e63a295
b4691b7
e63a295
 
0e86155
 
 
e63a295
b4691b7
0e86155
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e49e31a
 
0e86155
 
e49e31a
 
b4691b7
0e86155
 
e49e31a
 
 
 
0e86155
 
 
e49e31a
0e86155
e49e31a
e63a295
e49e31a
 
0e86155
e49e31a
 
e63a295
e49e31a
0e86155
b4691b7
0e86155
 
 
b4691b7
e49e31a
0e86155
e49e31a
0e86155
b4691b7
0e86155
 
 
b4691b7
e49e31a
0e86155
 
 
e49e31a
0e86155
 
 
b4691b7
 
 
5e3e294
b4691b7
 
 
 
 
0e86155
 
e49e31a
b4691b7
 
0e86155
 
 
 
 
 
 
 
 
 
 
 
b4691b7
 
e63a295
b4691b7
0e86155
 
e49e31a
b4691b7
 
0e86155
 
b4691b7
e49e31a
0e86155
b4691b7
0e86155
 
 
 
e49e31a
0e86155
e49e31a
0e86155
e49e31a
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
'use strict';
// Official HF PKCE redirect helpers. Only the one-use verifier crosses navigation;
// OAuth access tokens and profile details stay in this page's memory.
(() => {
  const node=id=>document.getElementById(id),flowKey='tiga.ald.hf.pkce.v1';
  const identityScopes=['openid','profile'],storageScopes=['openid','profile','write-repos'];
  let session=null,storageSession=null,locked=false,ready=false,config=null,exchanging=false;
  const message=text=>{node('hfIdentity').textContent=text;};
  const storageMessage=text=>{node('bucket_auth_status').textContent=text;};
  const scopeSet=value=>{
    if(typeof value!=='string'||!value.trim())return null;
    const items=value.trim().split(/\s+/),set=new Set(items);
    return set.size===items.length?set:null;
  };
  const exactScopes=(value,expected)=>{
    const scopes=scopeSet(value);
    return !!scopes&&scopes.size===expected.length&&expected.every(scope=>scopes.has(scope));
  };
  const storageEnabled=()=>ready&&config?.storage_enabled===true&&exactScopes(config.storage_scopes,storageScopes);
  function emitChange(){window.dispatchEvent(new Event('hf-auth-change'));}
  function render(){
    const anySession=!!session||!!storageSession;
    node('hfSignIn').hidden=!!session;
    node('hfSignIn').disabled=locked||exchanging||!ready;
    node('hfAccount').hidden=!session;
    node('hfAccount').textContent=session?'Connected as '+session.userInfo.preferred_username:'';
    node('hfSignOut').hidden=!anySession;
    node('hfSignOut').disabled=locked||exchanging||!anySession;
    node('hfSignOut').textContent=session?'Disconnect account':'Disconnect HF access';
    node('hfVerify').hidden=!session;
    node('hfVerify').disabled=locked||exchanging||!session;
    const authorize=node('bucket_authorize');
    if(authorize){authorize.hidden=!storageEnabled()||!!storageSession;authorize.disabled=locked||exchanging||!storageEnabled()||!!storageSession;}
    if(node('bucket_auth_unavailable'))node('bucket_auth_unavailable').hidden=storageEnabled();
    getStorageToken();
    if(node('bucket_auth_status'))node('bucket_auth_status').textContent=storageSession
      ?'HF bucket access authorized as '+storageSession.userInfo.preferred_username+'. Choose your private bucket below.'
      :'Not authorized. Bucket storage is optional.';
  }
  function clearAll(){session=null;storageSession=null;emitChange();render();}
  function clearStorage(){
    if(!storageSession)return;
    if(session===storageSession)session=null;
    storageSession=null;emitChange();render();
  }
  function getStorageToken(){
    if(storageSession&&storageSession.accessTokenExpiresAt.getTime()<=Date.now()){
      if(session===storageSession)session=null;
      storageSession=null;emitChange();storageMessage('HF authorization expired. Sign in or authorize access again to reconnect.');
      render();return null;
    }
    return storageSession?.accessToken??null;
  }
  window.HFAuth={
    get storageEnabled(){return storageEnabled();},
    get storageAuthorized(){return !!getStorageToken();},
    setBusy(value){locked=value;render();},
    credentials(){
      if(exchanging)throw Error('Complete HF sign-in before starting GPU');
      if(session&&session.accessTokenExpiresAt.getTime()<=Date.now()){
        clearAll();message('HF sign-in expired. Sign in again before an account GPU request.');
        throw Error('HF sign-in expired; sign in again');
      }
      return session?{token:session.accessToken}:{};
    },
    getStorageToken,
    getStorageUsername(){return getStorageToken()?storageSession.userInfo.preferred_username:null;},
    clearStorageAuthorization:clearStorage,
    verify(identity){
      if(session&&(!identity.authenticated||identity.auth_path!=='queue-bearer'||identity.username!==session.userInfo.preferred_username))
        throw Error('GPU queue visitor identity did not match your HF sign-in; no GPU request started');
      if(session)message('Account connection checked. GPU use remains subject to your HF allowance.');
    }
  };
  node('hfSignOut').onclick=()=>{clearAll();message('HF access disconnected from this app. CPU remains available. HF website sign-in is unchanged.');storageMessage('HF access disconnected. Authorize bucket access again to use storage.');};
  node('hfVerify').onclick=async()=>{
    try{await window.TrainingClient.verifyIdentity();}
    catch{message('Account connection check failed. No GPU request started. Disconnect and sign in again, or use CPU.');}
  };
  async function authorize(purpose){
    if(locked||exchanging||!ready)return;
    if(purpose==='storage'&&!storageEnabled())return;
    if(window.TrainingClient?.hasUnsavedState?.() &&
       !(await window.appConfirm('HF authorization navigates away. Selected files, a downloaded result and unsaved editor changes are lost; saved recipes remain. Continue, then reselect or recreate them?',{heading:'Continue to HF authorization',confirmLabel:'Continue to HF'})))return;
    exchanging=true;render();
    try{
      const state={},requested=purpose==='storage'?storageScopes:identityScopes;
      const helpers=await import('/hf-oauth.js');
      const url=await helpers.oauthLoginUrl({clientId:config.client_id,scopes:requested.join(' '),
        redirectUrl:location.origin+'/',localStorage:state});
      sessionStorage.setItem(flowKey,JSON.stringify({...state,started:Date.now(),purpose}));
      if(purpose==='storage')storageMessage('Opening HF permission request for bucket access. No password or personal token is entered here.');
      else message('Opening Hugging Face sign-in. No password or personal token is entered here.');
      location.href=url;
    }catch{
      exchanging=false;render();
      const text='HF authorization could not start. Tab-local sign-in storage is required; local export and CPU remain available.';
      if(purpose==='storage')storageMessage(text);else message(text);
    }
  }
  node('hfSignIn').onclick=()=>authorize('identity');
  node('bucket_authorize').onclick=()=>authorize('storage');
  const returned=location.href,params=new URL(returned).searchParams;
  const isCallback=params.has('code')||params.has('error');
  if(isCallback){history.replaceState(null,'','/');exchanging=true;}
  fetch('/api/auth/config',{cache:'no-store',credentials:'same-origin'}).then(async response=>{
    if(!response.ok)throw Error();config=await response.json();ready=config.enabled===true;
    if(isCallback){
      let flow;
      try{flow=JSON.parse(sessionStorage.getItem(flowKey));}
      finally{sessionStorage.removeItem(flowKey);}
      if(!ready||!flow?.nonce||!flow?.codeVerifier||!Number.isFinite(flow.started)||Date.now()-flow.started>300000||flow.started>Date.now()||!['identity','storage'].includes(flow.purpose))throw Error();
      if(flow.purpose==='storage'&&!storageEnabled())throw Error();
      const helpers=await import('/hf-oauth.js');
      const result=await helpers.oauthHandleRedirect({redirectedUrl:returned,nonce:flow.nonce,codeVerifier:flow.codeVerifier});
      flow.nonce=null;flow.codeVerifier=null;
      const allowed=flow.purpose==='storage'?storageScopes:identityScopes;
      if(!exactScopes(result.scope,allowed))throw Error();
      if(!result.accessToken||!result.userInfo?.preferred_username||!Number.isFinite(result.accessTokenExpiresAt?.getTime())||result.accessTokenExpiresAt.getTime()<=Date.now())throw Error();
      if(flow.purpose==='storage'){
        // The optional storage grant includes the ordinary identity scopes. Reuse
        // this one page-memory grant for both purposes so the user signs in once.
        session=result;storageSession=result;emitChange();
        storageMessage('HF bucket access authorized as '+result.userInfo.preferred_username+'. Choose your existing private bucket below.');
      }else{
        session=result;storageSession=null;emitChange();
        message('Account connected. You can choose GPU; your account is checked automatically before the run.');
      }
    }else{
      sessionStorage.removeItem(flowKey);
      message(ready?'CPU needs no sign-in. For your account GPU allowance, sign in before choosing files.':
        'HF sign-in is not configured on this deployment. CPU remains anonymous.');
      storageMessage(storageEnabled()?'Not authorized. Bucket storage is optional.':
        'HF storage authorization is unavailable on this deployment. Local bundle export remains available.');
    }
  }).catch(()=>{
    try{sessionStorage.removeItem(flowKey);}catch{}
    clearAll();message('HF sign-in unavailable, cancelled, expired, or not verified. No GPU request started; CPU remains anonymous.');
    storageMessage('HF authorization unavailable, cancelled, expired, or not verified. Bucket storage remains disconnected; local export is available.');
  }).finally(()=>{exchanging=false;render();});
  setInterval(()=>{
    let changed=false;
    if(session&&session.accessTokenExpiresAt.getTime()<=Date.now()){
      session=null;message('HF sign-in expired. Sign in again to identify account GPU requests. CPU remains available.');changed=true;
    }
    if(storageSession&&storageSession.accessTokenExpiresAt.getTime()<=Date.now()){
      storageSession=null;storageMessage('HF bucket authorization expired. Authorize access again to reconnect.');changed=true;
    }
    if(changed){emitChange();render();}
  },1000);
  render();
})();