banned && (int)$u->group_id===$group && (int)$u->u+(int)$u->d<(int)$u->transfer_enable && ($u->expired_at===null || (int)$u->expired_at>=time()); } public static function prepare(string $dir,int $group,bool $apply): array { return locked($dir,function()use($dir,$group,$apply){ if(!ServerGroup::find($group))throw new RuntimeException('Formal group missing'); $path=$dir.'/acceptance-user.json'; if(is_file($path) && readJson($path)['state']==='closed' && $apply){ $old=readJson($path);$prior=isset($old['id'])?User::find($old['id']):null; if($old['owned'] && $prior && (!$prior->banned || $prior->email!==$old['email'] || $prior->uuid!==$old['uuid'] || $prior->token!==$old['token'] || $prior->is_admin || (int)$prior->group_id!==$old['group']))throw new RuntimeException('Prior lease not revoked'); writeJson($dir.'/acceptance-closed-'.($old['id']??'absent').'.json',$old);unlink($path); } if(is_file($path)){ $j=readJson($path); if($j['group']!==$group || $j['state']==='closed')throw new RuntimeException('Lease closed or parameters changed; never reactivate'); $u=User::where('email',$j['email'])->first(); if(!$u && $j['owned'] && $j['state']==='prepared' && $apply){ $u=DB::transaction(fn()=>self::create($j),1); } if(!$u || $u->uuid!==$j['uuid'] || $u->token!==$j['token'] || !self::eligible($u,$group) || ($j['owned'] && $u->is_admin))throw new RuntimeException('Acceptance lease drifted'); if($j['owned'])self::limits($u,$j); $j['id']=$u->id;$j['state']='active';if($apply)writeJson($path,$j); return ['state'=>'ready','id'=>$u->id,'owned'=>$j['owned']]; } $u=User::where('group_id',$group)->where('banned',0)->whereRaw('u+dwhere(fn($q)=>$q->whereNull('expired_at')->orWhere('expired_at','>=',time()))->where('email','not like','node-qa-%')->where('email','not like','batch-acceptance-%')->orderBy('id')->first(); if(!$apply)return ['state'=>$u?'existing':'would_create']; $j=['group'=>$group,'state'=>'prepared','owned'=>!$u,'email'=>$u?->email??'batch-acceptance-'.substr(hash('sha256',$dir),0,12).'-'.Str::lower(Str::random(8)).'@example.invalid','uuid'=>$u?->uuid??(string)Str::uuid(),'token'=>$u?->token??Str::random(32),'quota'=>104857600,'expires_at'=>time()+3600]; // Persist identity before commit. A lost response resumes the same row, never another user. if(!$u && User::where('email',$j['email'])->exists())throw new RuntimeException('Unowned acceptance email conflict'); writeJson($path,$j); if(!$u)$u=DB::transaction(fn()=>self::create($j),1); $j['id']=$u->id;$j['state']='active';writeJson($path,$j); return ['state'=>'ready','id'=>$u->id,'owned'=>$j['owned']]; }); } private static function limits(User $u,array $j):void { if((int)$u->transfer_enable!==$j['quota'] || (int)$u->expired_at!==$j['expires_at'] || (int)$u->device_limit!==1)throw new RuntimeException('Owned lease limits changed; inspect before mutation'); } private static function create(array $j): User { $u=new User(['email'=>$j['email'],'uuid'=>$j['uuid'],'token'=>$j['token'],'password'=>password_hash(Str::random(40),PASSWORD_BCRYPT),'group_id'=>$j['group'],'transfer_enable'=>$j['quota'],'expired_at'=>$j['expires_at'],'banned'=>0,'is_admin'=>0,'remind_expire'=>0,'remind_traffic'=>0,'device_limit'=>1]);$u->saveQuietly();return $u; } public static function source(string $dir,int $group,bool $inspect=false): array { $j=readJson($dir.'/acceptance-user.json');$u=User::findOrFail($j['id']); if($j['group']!==$group || (!$inspect && $j['state']!=='active') || $u->uuid!==$j['uuid'] || $u->token!==$j['token'] || (!$inspect && !self::eligible($u,$group)) || ($inspect && !$j['owned'] && !self::eligible($u,$group)) || (int)$u->group_id!==$group)throw new RuntimeException('Acceptance user unavailable'); if($j['owned'])self::limits($u,$j); $nodes=[]; foreach(ServerService::getAvailableServers($u) as $s){ if(isset($s['ports']))throw new RuntimeException('Port-range nodes require an explicit verifier'); $model=Server::findOrFail($s['id']); if(!$inspect && !ServerService::getAvailableUsers($model)->contains('id',$u->id))throw new RuntimeException('User excluded by effective node filter'); $nodes[]=['id'=>$s['id'],'type'=>$s['type'],'tag'=>$s['name'],'relay'=>$s['host'],'relay_port'=>(int)$s['port'],'sni'=>$s['protocol_settings']['tls']['server_name']??null,'uuid'=>$s['password']]; } if($inspect){foreach($nodes as &$n)unset($n['uuid']);unset($n);return ['nodes'=>$nodes];} return ['user_id'=>$u->id,'owned'=>$j['owned'],'token'=>$u->token,'nodes'=>$nodes]; } public static function cleanup(string $dir,bool $apply): array { return locked($dir,function()use($dir,$apply){ $p=$dir.'/acceptance-user.json';if(!is_file($p))return ['state'=>'absent'];$j=readJson($p); $u=isset($j['id'])?User::find($j['id']):User::where('email',$j['email'])->first(); if($j['owned'] && $u){ if($u->email!==$j['email'] || $u->uuid!==$j['uuid'] || $u->token!==$j['token'] || $u->is_admin || (int)$u->group_id!==$j['group'])throw new RuntimeException('Cleanup ownership mismatch'); self::limits($u,$j); if($apply){$u->banned=true;$u->saveQuietly();} if($apply && !$u->fresh()->banned)throw new RuntimeException('Cleanup failed'); } if($apply){$j['state']='closed';$j['id']=$u?->id;writeJson($p,$j);} return ['state'=>$apply?'closed':'preview','owned'=>$j['owned'],'id'=>$u?->id]; }); } } if(realpath($_SERVER['SCRIPT_FILENAME']??'')===__FILE__){ $o=getopt('',['action:','dir:','group:','apply','help']); if(isset($o['help'])){echo "--action=prepare|source|snapshot|cleanup --dir=PRIVATE_DIR --group=EXISTING_ID [--apply]\n";exit;} try{ $dir=$o['dir']??'';$group=(int)($o['group']??0); if(!preg_match('~^/[A-Za-z0-9_./-]+$~D',$dir)||str_contains($dir,'..')||$group<1)throw new RuntimeException('Explicit private directory and group required'); umask(0077);if(!is_dir($dir))mkdir($dir,0700,true); if(is_link($dir)||(fileperms($dir)&0077))throw new RuntimeException('Private directory required'); boot();$apply=isset($o['apply']); $r=match($o['action']??''){'prepare'=>Acceptance::prepare($dir,$group,$apply),'source'=>Acceptance::source($dir,$group),'snapshot'=>Acceptance::source($dir,$group,true),'cleanup'=>Acceptance::cleanup($dir,$apply),default=>throw new RuntimeException('Unknown action')}; echo json_encode($r,JSON_THROW_ON_ERROR).PHP_EOL; }catch(\Throwable $e){fwrite(STDERR,json_encode(['state'=>'blocked','reason'=>'Inspect protected lease and effective authorization; no automatic overwrite']).PHP_EOL);exit(2);} }