codexunlimit 0.1.0-web.8 → 0.1.0-web.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -12,6 +12,7 @@ const LOCAL_DENIALS=new Set(['activation_required','license_signature_invalid','
12
12
  function serviceError(code,status=503,retryAfter=1){const error=new RelayError(status,code);error.transient=true;error.authoritative=false;error.retryAfter=retryAfter;return error;}
13
13
  function normalizeFailure(error){
14
14
  const code=typeof error?.code==='string'?error.code:error?.message;
15
+ if(code==='license_lease_expired')return serviceError(code,503,error?.retryAfter??1);
15
16
  if(error?.transient||error?.status>=500||error?.status===429||['license_server_unavailable','license_response_invalid','license_service_busy','try_later','ingress_rate_limit','ingress_busy','ingress_capacity'].includes(code))return serviceError(code==='license_service_busy'||error?.status===429?'license_service_busy':'license_server_unavailable',error?.status===429?429:503,error?.retryAfter??1);
16
17
  if(TERMINAL_DENIALS.has(code)||LOCAL_DENIALS.has(code)){const failure=new RelayError(403,code);failure.authoritative=error?.authoritative!==false;return failure;}
17
18
  return serviceError('license_server_unavailable');
@@ -67,14 +68,15 @@ export class LicenseClient {
67
68
  this.state=state;this.publicKey=publicKey;this.request=request;this.device=device??loadDevice(state);this.now=now;this.pending=null;this.nextAttempt=0;this.lastFailure=null;this.failures=0;
68
69
  this.wallStart=now();this.monotonicStart=performance.now();
69
70
  }
70
- check(envelope=this.state.get('license.lease'),feature='desktop'){
71
+ check(envelope=this.state.get('license.lease'),feature='desktop',{allowExpired=false}={}){
71
72
  let claims;try{claims=verifyEnvelope(envelope,this.publicKey);}catch{throw Error('license_signature_invalid');}
72
73
  const now=this.now(),last=this.state.get('license.clock')??0;
73
74
  if(now<last-30000||now<this.wallStart+performance.now()-this.monotonicStart-30000)throw Error('clock_rollback_detected');
74
75
  if(claims.kind!=='tdh-relay-lease-v1'||claims.product!=='tdh-relay'||claims.deviceId!==this.device.deviceId||
75
76
  claims.keyHash!==hash(this.device.publicKey)||!Array.isArray(claims.features)||!claims.features.includes(feature)||!Number.isFinite(claims.licenseExpiresAt)||
76
77
  !Number.isFinite(claims.issuedAt)||!Number.isFinite(claims.expiresAt)||claims.issuedAt>now+60000||
77
- claims.expiresAt<=now||claims.expiresAt>claims.issuedAt+600000||claims.expiresAt>claims.licenseExpiresAt)throw Error('license_invalid_or_expired');
78
+ claims.expiresAt<=claims.issuedAt||claims.expiresAt>claims.issuedAt+600000||claims.expiresAt>claims.licenseExpiresAt)throw Error('license_invalid_or_expired');
79
+ if(!allowExpired&&claims.expiresAt<=now)throw serviceError('license_lease_expired');
78
80
  if(now>last+10000)this.state.set('license.clock',now);
79
81
  return claims;
80
82
  }
@@ -98,9 +100,12 @@ export class LicenseClient {
98
100
  const activationId=this.state.get('license.activation');if(!activationId)throw Error('activation_required');
99
101
  this.pending=(async()=>{
100
102
  try{
101
- const proof=await this.proof('renew:'+activationId);
102
- const response=await this.request('renew',{...proof,activationId});
103
- try{return this.save(response);}catch(error){error.authoritative=false;throw error;}
103
+ for(let attempt=0;;attempt++){
104
+ const proof=await this.proof('renew:'+activationId);
105
+ const response=await this.request('renew',{...proof,activationId});
106
+ try{return this.save(response);}
107
+ catch(error){error.authoritative=false;if(error.code!=='license_lease_expired'||attempt>=1)throw error;}
108
+ }
104
109
  }catch(error){
105
110
  const failure=normalizeFailure(error);this.lastFailure=failure;
106
111
  if(TERMINAL_DENIALS.has(failure.code)&&failure.authoritative){
@@ -121,7 +126,21 @@ export class LicenseClient {
121
126
  }else if((!current||online)&&this.lastFailure)throw this.lastFailure;
122
127
  }
123
128
  if(this.state.get('license.denied'))throw new RelayError(403,'license_revoked');
124
- try{return this.check(undefined,feature);}catch(error){if(this.lastFailure?.transient)throw this.lastFailure;throw new RelayError(403,error.message);}
129
+ try{return this.check(undefined,feature);}catch(error){if(this.lastFailure?.transient)throw this.lastFailure;throw normalizeFailure(error);}
130
+ }
131
+ async forRequest(feature='desktop'){
132
+ try{if(!this.state.get('license.denied'))return this.check(undefined,feature,{allowExpired:true});}catch{}
133
+ return this.ensure(feature);
134
+ }
135
+ acceptLease(encoded){
136
+ if(typeof encoded!=='string'||encoded.length>4096||this.state.get('license.denied'))return false;
137
+ try{
138
+ const envelope=JSON.parse(Buffer.from(encoded,'base64url')),claims=this.check(envelope);
139
+ if(claims.activationId!==this.state.get('license.activation'))return false;
140
+ const current=this.check(undefined,'desktop',{allowExpired:true});
141
+ if(claims.licenseId!==current.licenseId||claims.issuedAt<current.issuedAt||claims.issuedAt===current.issuedAt&&claims.expiresAt<=current.expiresAt)return false;
142
+ this.save({lease:envelope});return true;
143
+ }catch{return false;}
125
144
  }
126
145
  status(){
127
146
  try{const c=this.check();return {active:!this.state.get('license.denied'),licenseId:c.licenseId,activationId:c.activationId,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codexunlimit",
3
- "version": "0.1.0-web.8",
3
+ "version": "0.1.0-web.9",
4
4
  "private": false,
5
5
  "description": "Codex Unlimit — trò chuyện và quản lý kết nối trên máy của bạn",
6
6
  "type": "module",
package/product.mjs CHANGED
@@ -1,4 +1,4 @@
1
- export const VERSION='0.1.0-web.8';
1
+ export const VERSION='0.1.0-web.9';
2
2
  export const PRODUCT='codexunlimit-connector';
3
3
  export const DEFAULT_PORT=18457;
4
4
  export const LICENSE_ORIGIN='https://codexunlimit.tudonghoa.me';
package/relay.mjs CHANGED
@@ -8,7 +8,7 @@ import {isImageRoute,gatewayRequest} from './image-http.mjs';
8
8
 
9
9
  const CONTEXT_HEADERS=['content-type','content-encoding','chatgpt-account-id','session_id','session-id','conversation_id','originator','version','openai-beta','x-codex-beta-features','x-codex-parent-thread-id','x-codex-turn-metadata','x-codex-turn-state'];
10
10
  export function signedHeaders(license,state,method,route,authorization,body=Buffer.alloc(0),headers=new Headers()){
11
- const envelope=state.get('license.lease');license.check(envelope);
11
+ const envelope=state.get('license.lease');license.check(envelope,'desktop',{allowExpired:true});
12
12
  const timestamp=Date.now(),nonce=crypto.randomBytes(24).toString('base64url'),key=license.device.privateKey;
13
13
  const bodyHash=hash(body??Buffer.alloc(0));
14
14
  const contextHeaders=isImageRoute(route)?[...CONTEXT_HEADERS,'x-codex-image-turn-id']:CONTEXT_HEADERS;
@@ -64,9 +64,10 @@ export function createConnector(state,{license,localApp,pool,onStop=()=>{},prepa
64
64
  const chunks=[];let size=0;for await(const chunk of req){size+=chunk.length;if(size>limit)throw Object.assign(Error('request_too_large'),{status:413});chunks.push(chunk);}
65
65
  clearTimeout(bodyTimer);const payload=req.method==='POST'?Buffer.concat(chunks):undefined;chunks.length=0;
66
66
  // Sign only after upload completes, so a slow upload cannot age the proof out.
67
- controller.signal.throwIfAborted();await license.ensure('desktop');controller.signal.throwIfAborted();
67
+ controller.signal.throwIfAborted();await license.forRequest('desktop');controller.signal.throwIfAborted();
68
68
  for(const [key,value]of Object.entries(signedHeaders(license,state,req.method,req.url,authorization,payload,headers)))headers.set(key,value);
69
69
  const response=await request(origin+req.url,{method:req.method,headers,body:payload,redirect:'error',signal:AbortSignal.any([controller.signal,AbortSignal.timeout(isImageRoute(req.url)?630000:1800000)])});
70
+ license.acceptLease(response.headers.get('x-cu-lease'));
70
71
  pool?.observe({token:authorization.slice(7),accountId:headers.get('chatgpt-account-id')??undefined},response);
71
72
  if(response.status>=400&&!(response.headers.get('content-type')??'').toLowerCase().startsWith('application/json')){
72
73
  response.body?.cancel().catch(()=>{});status=response.status===429?429:503;const wait=Number(response.headers.get('retry-after'));res.setHeader('retry-after',String(Number.isFinite(wait)&&wait>0?Math.min(60,Math.ceil(wait)):3));
package/web/messages.js CHANGED
@@ -19,6 +19,7 @@ export const errors={
19
19
  request_integrity_failed:'Yêu cầu chưa được xác thực. Cập nhật Codex Unlimit rồi thử lại.',
20
20
  license_server_unavailable:'Chưa liên lạc được máy chủ kiểm tra key. Kiểm tra mạng rồi chọn Kiểm tra thời hạn.',
21
21
  license_service_busy:'Máy chủ kiểm tra key đang bận. Chờ vài giây rồi thử lại; đây không phải thông báo key hết hạn.',
22
+ license_lease_expired:'Phiên xác thực tạm thời đã hết hạn. Chọn Kiểm tra thời hạn để lấy phiên mới; nếu lặp lại, kiểm tra ngày giờ trên máy.',
22
23
  license_invalid_or_expired:'Key đã hết hạn hoặc bị thu hồi. Liên hệ nơi cấp key.',
23
24
  license_revoked:'Key đã bị thu hồi. Liên hệ nơi cấp key.',
24
25
  device_limit_reached:'Key đã đủ số máy được phép dùng. Mở trang khách hàng để kiểm tra thiết bị.',