Spaces:
Running on Zero
Running on Zero
Use standard HF PKCE redirect across OAuth opener isolation
Browse files- DATA_POLICY.md +7 -4
- RELEASE_MANIFEST.json +4 -5
- STORAGE_BOUNDARY.md +2 -1
- hf-auth.js +40 -36
- server.py +2 -5
DATA_POLICY.md
CHANGED
|
@@ -35,11 +35,14 @@ of physical RAM/disk erasure, prevention of swap, or deletion of response buffer
|
|
| 35 |
No claim is made about hosting-provider logs or infrastructure retention.
|
| 36 |
|
| 37 |
Optional HF sign-in uses the official HF OAuth authorization-code/PKCE flow with
|
| 38 |
-
only openid/profile scopes.
|
| 39 |
page memory, not cookies, localStorage, recipes, or a server session database.
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
|
|
|
|
|
|
|
|
|
| 43 |
application's deletion guarantee. The app does not request email, billing,
|
| 44 |
repository access, or persistent bucket access.
|
| 45 |
|
|
|
|
| 35 |
No claim is made about hosting-provider logs or infrastructure retention.
|
| 36 |
|
| 37 |
Optional HF sign-in uses the official HF OAuth authorization-code/PKCE flow with
|
| 38 |
+
only openid/profile scopes. Access tokens and basic profile details live in browser
|
| 39 |
page memory, not cookies, localStorage, recipes, or a server session database.
|
| 40 |
+
Only the one-use PKCE verifier/nonce and start time are temporarily saved in
|
| 41 |
+
tab-local sessionStorage to survive the full-page redirect, with a five-minute
|
| 42 |
+
acceptance limit; they are removed on return, error, or a fresh app load. Closing
|
| 43 |
+
the tab loses them. HF handles password entry and authorization. The callback
|
| 44 |
+
briefly carries a one-use authorization code and state in its URL; the app clears
|
| 45 |
+
that URL before loading the OAuth helper. HF/proxy infrastructure handling is outside this
|
| 46 |
application's deletion guarantee. The app does not request email, billing,
|
| 47 |
repository access, or persistent bucket access.
|
| 48 |
|
RELEASE_MANIFEST.json
CHANGED
|
@@ -2,13 +2,13 @@
|
|
| 2 |
"backend": "cpu+zerogpu",
|
| 3 |
"files": {
|
| 4 |
"BETA_TESTING.md": "d511965b9d675a092571f8815348da991ef1ef6be3766266c1f98ff393fd3a4d",
|
| 5 |
-
"DATA_POLICY.md": "
|
| 6 |
"FRONTEND_SOURCE.json": "1e31b153757fd42c3bc2abaa1982a44e19a2ed7dc259d32781f36bb9a3ee10b7",
|
| 7 |
"GODOT_LICENSE": "fb2b3bb82c60f1e76841ba2122eb902f8e39c151476fe21ce10c9a05d1045304",
|
| 8 |
"IMPORTER_BUILD_RECEIPT.json": "4ba5c533cc617105478e4d85c83f456a7dfa68b1388049624619871ba9e42e9d",
|
| 9 |
"LICENSE": "53d5851b6440feeae36f72a5f57fcab37858c48b7084765ece4b0c54b9a28ed8",
|
| 10 |
"README.md": "11abed635d4192b5ea8d8891fc38e3a60989ad5b4696683d0321f3bc12eba20f",
|
| 11 |
-
"STORAGE_BOUNDARY.md": "
|
| 12 |
"app.py": "704f789a41846b0a5bc1d3f6f5305d7ac5a6b1719ff77c23585b89fd3e68d55d",
|
| 13 |
"app_release.py": "7482559c667a665fd3cb6aa8af6c78422a822b515e14589db8306f7fd4860a8b",
|
| 14 |
"assets/formula_template.json": "80f745df82bb7be45af52785fd582460bcddc63c5a4e1d8375d85b3122db4c6b",
|
|
@@ -18,12 +18,11 @@
|
|
| 18 |
"client.js": "79a34470942132a4e4a4100ae92b805ff02f7fe344e7962cfcff6a1208cef54a",
|
| 19 |
"delivery.py": "4f5a4e45df92e5cbfdef6eb449ede9ba26217fe24da376fafddae5cbbb73847a",
|
| 20 |
"gpu_tickets.py": "c32cb5de751d49d3e1cd970c7d6328a42ebe71602dd9fd3788c872eef42611da",
|
| 21 |
-
"hf-auth
|
| 22 |
-
"hf-auth.js": "d39981c98eec48d941a8e8b31b49ba0c44b7debe628c92681dfd26324782e545",
|
| 23 |
"hf_identity.py": "6993d0238ada835606dbdc3275c17a3fe5a5cac5ad2156ab78230acf2d051eba",
|
| 24 |
"index.html": "9e5b6bd69dd587f0e5c0f551b291de4d6a729e601916042cde0449ba2162bee8",
|
| 25 |
"requirements.txt": "9b532747e51bd052e82090ece860d6dafe027407f4512dc74272c148ea104c22",
|
| 26 |
-
"server.py": "
|
| 27 |
"training_config.py": "2b9768876fdc60b16afb45b98023c6c31666e30f7c56f1292f6071775e6da0c5",
|
| 28 |
"vendor/gradio-client/LICENSE": "c71d239df91726fc519c6eb72d318ec65820627232b2f796219e87dcf35d0ab4",
|
| 29 |
"vendor/gradio-client/dist/index.min.js": "fdd776db8633354f118fe2908c980592283968737e6b91dc3aa86d648da5c3fd",
|
|
|
|
| 2 |
"backend": "cpu+zerogpu",
|
| 3 |
"files": {
|
| 4 |
"BETA_TESTING.md": "d511965b9d675a092571f8815348da991ef1ef6be3766266c1f98ff393fd3a4d",
|
| 5 |
+
"DATA_POLICY.md": "8a954c46d39783582b2d4fbaa0fa2510e2e13446b0dff0b3c66f8817341b0289",
|
| 6 |
"FRONTEND_SOURCE.json": "1e31b153757fd42c3bc2abaa1982a44e19a2ed7dc259d32781f36bb9a3ee10b7",
|
| 7 |
"GODOT_LICENSE": "fb2b3bb82c60f1e76841ba2122eb902f8e39c151476fe21ce10c9a05d1045304",
|
| 8 |
"IMPORTER_BUILD_RECEIPT.json": "4ba5c533cc617105478e4d85c83f456a7dfa68b1388049624619871ba9e42e9d",
|
| 9 |
"LICENSE": "53d5851b6440feeae36f72a5f57fcab37858c48b7084765ece4b0c54b9a28ed8",
|
| 10 |
"README.md": "11abed635d4192b5ea8d8891fc38e3a60989ad5b4696683d0321f3bc12eba20f",
|
| 11 |
+
"STORAGE_BOUNDARY.md": "85442bbaffdf1208a1b7d3359cf1f0a2000a2d1ca46594b3a5fe23d52db41e00",
|
| 12 |
"app.py": "704f789a41846b0a5bc1d3f6f5305d7ac5a6b1719ff77c23585b89fd3e68d55d",
|
| 13 |
"app_release.py": "7482559c667a665fd3cb6aa8af6c78422a822b515e14589db8306f7fd4860a8b",
|
| 14 |
"assets/formula_template.json": "80f745df82bb7be45af52785fd582460bcddc63c5a4e1d8375d85b3122db4c6b",
|
|
|
|
| 18 |
"client.js": "79a34470942132a4e4a4100ae92b805ff02f7fe344e7962cfcff6a1208cef54a",
|
| 19 |
"delivery.py": "4f5a4e45df92e5cbfdef6eb449ede9ba26217fe24da376fafddae5cbbb73847a",
|
| 20 |
"gpu_tickets.py": "c32cb5de751d49d3e1cd970c7d6328a42ebe71602dd9fd3788c872eef42611da",
|
| 21 |
+
"hf-auth.js": "743fa2b62c611fcc6064664691e133efb5d163412955b2a9d0a562a428412864",
|
|
|
|
| 22 |
"hf_identity.py": "6993d0238ada835606dbdc3275c17a3fe5a5cac5ad2156ab78230acf2d051eba",
|
| 23 |
"index.html": "9e5b6bd69dd587f0e5c0f551b291de4d6a729e601916042cde0449ba2162bee8",
|
| 24 |
"requirements.txt": "9b532747e51bd052e82090ece860d6dafe027407f4512dc74272c148ea104c22",
|
| 25 |
+
"server.py": "dda63ffe8e9ea0191c940fcb996ed790cbb21b8be96e83aeca2a67c33ce45582",
|
| 26 |
"training_config.py": "2b9768876fdc60b16afb45b98023c6c31666e30f7c56f1292f6071775e6da0c5",
|
| 27 |
"vendor/gradio-client/LICENSE": "c71d239df91726fc519c6eb72d318ec65820627232b2f796219e87dcf35d0ab4",
|
| 28 |
"vendor/gradio-client/dist/index.min.js": "fdd776db8633354f118fe2908c980592283968737e6b91dc3aa86d648da5c3fd",
|
STORAGE_BOUNDARY.md
CHANGED
|
@@ -12,7 +12,8 @@ same settings contract is used independently of the selected storage policy.
|
|
| 12 |
| Single-run output | Memory result store and random job key | Receipt ACK in automatic mode, manual delete, fixed 30-minute expiry, or shutdown |
|
| 13 |
| Batch intermediates | Same job contract, always automatic | Verified size/hash and deletion ACK before advancement |
|
| 14 |
| Received candidate / receipt | Browser page memory | Explicit discard, replacement, page reload/close; explicit export to disk |
|
| 15 |
-
| Optional HF identity / token
|
|
|
|
| 16 |
| User bucket | Not implemented | Must require an explicit opt-in adapter and separate retention policy |
|
| 17 |
|
| 18 |
`recipes.cjs` has no host/network dependency. Its `createStore(storage)` receives
|
|
|
|
| 12 |
| Single-run output | Memory result store and random job key | Receipt ACK in automatic mode, manual delete, fixed 30-minute expiry, or shutdown |
|
| 13 |
| Batch intermediates | Same job contract, always automatic | Verified size/hash and deletion ACK before advancement |
|
| 14 |
| Received candidate / receipt | Browser page memory | Explicit discard, replacement, page reload/close; explicit export to disk |
|
| 15 |
+
| Optional HF identity / token | Browser page memory, separate from job/recipe storage | Sign out, expiry, or page reload/close; no app auth cookie or server session database |
|
| 16 |
+
| One-use HF PKCE verifier/nonce | Tab-local sessionStorage for the sign-in redirect only | Five-minute acceptance limit; removed on return/error/fresh app load or tab close; never stores access tokens or files |
|
| 17 |
| User bucket | Not implemented | Must require an explicit opt-in adapter and separate retention policy |
|
| 18 |
|
| 19 |
`recipes.cjs` has no host/network dependency. Its `createStore(storage)` receives
|
hf-auth.js
CHANGED
|
@@ -1,19 +1,20 @@
|
|
| 1 |
'use strict';
|
| 2 |
-
// Official HF PKCE helpers
|
|
|
|
| 3 |
(() => {
|
| 4 |
-
const node=id=>document.getElementById(id);
|
| 5 |
-
let session=null,
|
| 6 |
const message=text=>{node('hfIdentity').textContent=text;};
|
| 7 |
function render(){
|
| 8 |
-
node('hfSignIn').disabled=locked||
|
| 9 |
-
node('hfSignOut').disabled=locked||
|
| 10 |
-
node('hfVerify').disabled=locked||
|
| 11 |
}
|
| 12 |
function clear(){session=null;window.dispatchEvent(new Event('hf-auth-change'));render();}
|
| 13 |
window.HFAuth={
|
| 14 |
setBusy(value){locked=value;render();},
|
| 15 |
credentials(){
|
| 16 |
-
if(
|
| 17 |
if(session && session.accessTokenExpiresAt.getTime()<=Date.now()){
|
| 18 |
clear();message('HF sign-in expired. Sign in again before an account GPU request.');
|
| 19 |
throw Error('HF sign-in expired; sign in again');
|
|
@@ -32,49 +33,52 @@
|
|
| 32 |
catch{message('Queue identity check failed. No GPU request started. Sign in again or use CPU.');}
|
| 33 |
};
|
| 34 |
node('hfSignIn').onclick=async()=>{
|
| 35 |
-
if(locked||
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
pending={popup,state,started:Date.now()};render();
|
| 40 |
try{
|
|
|
|
| 41 |
const helpers=await import('/hf-oauth.js');
|
| 42 |
const url=await helpers.oauthLoginUrl({clientId:config.client_id,scopes:'openid profile',
|
| 43 |
-
redirectUrl:location.origin+'/
|
| 44 |
-
|
| 45 |
-
|
|
|
|
| 46 |
}catch{
|
| 47 |
-
|
| 48 |
}
|
| 49 |
};
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 56 |
const helpers=await import('/hf-oauth.js');
|
| 57 |
-
const result=await helpers.oauthHandleRedirect({redirectedUrl:returned.
|
| 58 |
-
|
| 59 |
const scopes=new Set(result.scope.split(/\s+/));
|
| 60 |
if([...scopes].some(scope=>!['openid','profile'].includes(scope)))throw Error();
|
| 61 |
if(!result.accessToken||!result.userInfo.preferred_username||result.accessTokenExpiresAt.getTime()<=Date.now())throw Error();
|
| 62 |
session=result;window.dispatchEvent(new Event('hf-auth-change'));
|
| 63 |
message('Signed in with HF: '+result.userInfo.preferred_username+'. Verify queue identity before GPU use.');
|
| 64 |
-
}
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
}
|
| 69 |
-
})
|
|
|
|
|
|
|
|
|
|
| 70 |
setInterval(()=>{
|
| 71 |
-
if(
|
| 72 |
-
|
| 73 |
}
|
| 74 |
},1000);
|
| 75 |
-
fetch('/api/auth/config',{cache:'no-store',credentials:'omit'}).then(async response=>{
|
| 76 |
-
if(!response.ok)throw Error();config=await response.json();ready=config.enabled===true;render();
|
| 77 |
-
message(ready?'Optional: sign in with HF to identify account GPU requests. CPU needs no sign-in. Token stays in this page’s memory until sign-out, expiry, or page close.':
|
| 78 |
-
'HF sign-in is not configured on this deployment. CPU remains anonymous.');
|
| 79 |
-
}).catch(()=>{message('HF sign-in unavailable. CPU remains anonymous.');render();});
|
| 80 |
})();
|
|
|
|
| 1 |
'use strict';
|
| 2 |
+
// Official HF PKCE redirect helpers. Only the one-use verifier crosses navigation;
|
| 3 |
+
// account access tokens and profile details stay in this page's memory.
|
| 4 |
(() => {
|
| 5 |
+
const node=id=>document.getElementById(id),flowKey='tiga.ald.hf.pkce.v1';
|
| 6 |
+
let session=null,locked=false,ready=false,config=null,exchanging=false;
|
| 7 |
const message=text=>{node('hfIdentity').textContent=text;};
|
| 8 |
function render(){
|
| 9 |
+
node('hfSignIn').disabled=locked||exchanging||!ready;
|
| 10 |
+
node('hfSignOut').disabled=locked||exchanging||!session;
|
| 11 |
+
node('hfVerify').disabled=locked||exchanging||!session;
|
| 12 |
}
|
| 13 |
function clear(){session=null;window.dispatchEvent(new Event('hf-auth-change'));render();}
|
| 14 |
window.HFAuth={
|
| 15 |
setBusy(value){locked=value;render();},
|
| 16 |
credentials(){
|
| 17 |
+
if(exchanging)throw Error('Complete HF sign-in before starting GPU');
|
| 18 |
if(session && session.accessTokenExpiresAt.getTime()<=Date.now()){
|
| 19 |
clear();message('HF sign-in expired. Sign in again before an account GPU request.');
|
| 20 |
throw Error('HF sign-in expired; sign in again');
|
|
|
|
| 33 |
catch{message('Queue identity check failed. No GPU request started. Sign in again or use CPU.');}
|
| 34 |
};
|
| 35 |
node('hfSignIn').onclick=async()=>{
|
| 36 |
+
if(locked||exchanging||!ready)return;
|
| 37 |
+
if((node('track').files.length||node('network').files.length) &&
|
| 38 |
+
!confirm('HF sign-in navigates away. Selected files and unsaved editor changes are lost; saved recipes remain. Continue, then reselect files?'))return;
|
| 39 |
+
exchanging=true;render();
|
|
|
|
| 40 |
try{
|
| 41 |
+
const state={};
|
| 42 |
const helpers=await import('/hf-oauth.js');
|
| 43 |
const url=await helpers.oauthLoginUrl({clientId:config.client_id,scopes:'openid profile',
|
| 44 |
+
redirectUrl:location.origin+'/',localStorage:state});
|
| 45 |
+
sessionStorage.setItem(flowKey,JSON.stringify({...state,started:Date.now()}));
|
| 46 |
+
message('Opening Hugging Face sign-in. No password or personal token is entered here.');
|
| 47 |
+
location.href=url;
|
| 48 |
}catch{
|
| 49 |
+
exchanging=false;render();message('HF sign-in could not start. Tab-local sign-in storage is required; CPU remains available.');
|
| 50 |
}
|
| 51 |
};
|
| 52 |
+
const returned=location.href,params=new URL(returned).searchParams;
|
| 53 |
+
const isCallback=params.has('code')||params.has('error');
|
| 54 |
+
if(isCallback){history.replaceState(null,'','/');exchanging=true;}
|
| 55 |
+
fetch('/api/auth/config',{cache:'no-store',credentials:'omit'}).then(async response=>{
|
| 56 |
+
if(!response.ok)throw Error();config=await response.json();ready=config.enabled===true;
|
| 57 |
+
if(isCallback){
|
| 58 |
+
let flow;
|
| 59 |
+
try{flow=JSON.parse(sessionStorage.getItem(flowKey));}
|
| 60 |
+
finally{sessionStorage.removeItem(flowKey);}
|
| 61 |
+
if(!ready||!flow?.nonce||!flow?.codeVerifier||Date.now()-flow.started>300000||flow.started>Date.now())throw Error();
|
| 62 |
const helpers=await import('/hf-oauth.js');
|
| 63 |
+
const result=await helpers.oauthHandleRedirect({redirectedUrl:returned,nonce:flow.nonce,codeVerifier:flow.codeVerifier});
|
| 64 |
+
flow.nonce=null;flow.codeVerifier=null;
|
| 65 |
const scopes=new Set(result.scope.split(/\s+/));
|
| 66 |
if([...scopes].some(scope=>!['openid','profile'].includes(scope)))throw Error();
|
| 67 |
if(!result.accessToken||!result.userInfo.preferred_username||result.accessTokenExpiresAt.getTime()<=Date.now())throw Error();
|
| 68 |
session=result;window.dispatchEvent(new Event('hf-auth-change'));
|
| 69 |
message('Signed in with HF: '+result.userInfo.preferred_username+'. Verify queue identity before GPU use.');
|
| 70 |
+
}else{
|
| 71 |
+
sessionStorage.removeItem(flowKey);
|
| 72 |
+
message(ready?'Optional: sign in with HF before choosing files. CPU needs no sign-in. Token stays in page memory; only a one-use sign-in verifier crosses the redirect in tab-local storage.':
|
| 73 |
+
'HF sign-in is not configured on this deployment. CPU remains anonymous.');
|
| 74 |
}
|
| 75 |
+
}).catch(()=>{
|
| 76 |
+
try{sessionStorage.removeItem(flowKey);}catch{}
|
| 77 |
+
clear();message('HF sign-in unavailable, cancelled, expired, or not verified. No GPU request started; CPU remains anonymous.');
|
| 78 |
+
}).finally(()=>{exchanging=false;render();});
|
| 79 |
setInterval(()=>{
|
| 80 |
+
if(session&&session.accessTokenExpiresAt.getTime()<=Date.now()){
|
| 81 |
+
clear();message('HF sign-in expired. Sign in again to identify account GPU requests. CPU remains available.');
|
| 82 |
}
|
| 83 |
},1000);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 84 |
})();
|
server.py
CHANGED
|
@@ -19,10 +19,6 @@ def create_app(worker=run_reported,store=None,gpu_worker=None):
|
|
| 19 |
@app.get('/api/auth/config')
|
| 20 |
def auth_config():
|
| 21 |
return JSONResponse(oauth_config(),headers={'Cache-Control':'no-store'})
|
| 22 |
-
@app.get('/auth/callback')
|
| 23 |
-
def auth_callback():
|
| 24 |
-
return FileResponse(ROOT/'hf-auth-callback.html',media_type='text/html',
|
| 25 |
-
headers={'Cache-Control':'no-store','Referrer-Policy':'no-referrer'})
|
| 26 |
@app.get('/hf-auth.js')
|
| 27 |
def auth_script(): return FileResponse(ROOT/'hf-auth.js',media_type='text/javascript')
|
| 28 |
@app.get('/hf-oauth.js')
|
|
@@ -36,7 +32,8 @@ def create_app(worker=run_reported,store=None,gpu_worker=None):
|
|
| 36 |
# iframe subresource access can fail before an external script runs.
|
| 37 |
script=(ROOT/'client.js').read_text().replace('</script','<\\/script')
|
| 38 |
return HTMLResponse((ROOT/'index.html').read_text().replace(
|
| 39 |
-
'<script src="/client.js"></script>','<script>'+script+'</script>')
|
|
|
|
| 40 |
@app.get('/client.js')
|
| 41 |
def script(): return FileResponse(ROOT/'client.js',media_type='text/javascript')
|
| 42 |
@app.get('/policy')
|
|
|
|
| 19 |
@app.get('/api/auth/config')
|
| 20 |
def auth_config():
|
| 21 |
return JSONResponse(oauth_config(),headers={'Cache-Control':'no-store'})
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
@app.get('/hf-auth.js')
|
| 23 |
def auth_script(): return FileResponse(ROOT/'hf-auth.js',media_type='text/javascript')
|
| 24 |
@app.get('/hf-oauth.js')
|
|
|
|
| 32 |
# iframe subresource access can fail before an external script runs.
|
| 33 |
script=(ROOT/'client.js').read_text().replace('</script','<\\/script')
|
| 34 |
return HTMLResponse((ROOT/'index.html').read_text().replace(
|
| 35 |
+
'<script src="/client.js"></script>','<script>'+script+'</script>'),
|
| 36 |
+
headers={'Referrer-Policy':'no-referrer','Cache-Control':'no-store'})
|
| 37 |
@app.get('/client.js')
|
| 38 |
def script(): return FileResponse(ROOT/'client.js',media_type='text/javascript')
|
| 39 |
@app.get('/policy')
|