codexunlimit 0.1.0-web.7 → 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.
package/README.md CHANGED
@@ -1,13 +1,13 @@
1
1
  # Codex Unlimit · Web trên máy
2
2
 
3
- Cài một gói trên Windows hoặc macOS, mở trình duyệt và bắt đầu sử dụng. Cần Node.js 24.22.1 trở lên.
3
+ Cài một gói trên Windows hoặc macOS, mở trình duyệt và bắt đầu sử dụng. Hỗ trợ Node.js 22 từ 22.15.0, hoặc Node.js 24 trở lên.
4
4
 
5
5
  ## Cài và mở
6
6
 
7
- Mở PowerShell (Windows) hoặc Terminal (macOS/Linux). Cài qua npm từ URL cố định của sản phẩm:
7
+ Mở PowerShell (Windows) hoặc Terminal (macOS/Linux). Cài và mở bằng hai dòng:
8
8
 
9
9
  ```sh
10
- npm install -g https://codexunlimit.tudonghoa.me/codexunlimit.tgz
10
+ npm i -g codexunlimit
11
11
  codexunlimit
12
12
  ```
13
13
 
@@ -15,7 +15,7 @@ Trang tự mở. Chọn **Cài đặt & hỗ trợ**, nhập mã sử dụng, đ
15
15
 
16
16
  Trên Windows, có thể dùng `npm.cmd` và `codexunlimit.cmd` nếu PowerShell chặn file `.ps1`.
17
17
 
18
- Trang cài đặt công bố nguồn đang có hiệu lực. Khi gói chính thức trên npmjs đã được xác nhận tại đó, có thể dùng `npm i -g codexunlimit` hoặc thêm `@0.1.0-web.7` để cố định phiên bản. URL sản phẩm vẫn là phương án dự phòng. Không coi tên trần trên registry là chính thức trước thông báo xuất bản. Gói không có hook cài đặt tự chạy. Cần Node/npm có sẵn; không tải/chạy PowerShell từ web hay yêu cầu bộ cài .exe riêng. Kiểm tra môi trường bằng `codexunlimit doctor` (hoặc `--json`); lệnh không đọc token ChatGPT.
18
+ Trang cài đặt công bố nguồn đang có hiệu lực. Khi gói chính thức trên npmjs đã được xác nhận tại đó, có thể dùng `npm i -g codexunlimit` hoặc thêm `@0.1.0-web.8` để cố định phiên bản. URL sản phẩm vẫn là phương án dự phòng. Không coi tên trần trên registry là chính thức trước thông báo xuất bản. Gói không có hook cài đặt tự chạy. Cần Node/npm có sẵn; không tải/chạy PowerShell từ web hay yêu cầu bộ cài .exe riêng. Kiểm tra môi trường bằng `codexunlimit doctor` (hoặc `--json`); lệnh không đọc token ChatGPT.
19
19
 
20
20
  ## Ảnh và độ ổn định · web.7
21
21
 
package/doctor.mjs CHANGED
@@ -6,7 +6,7 @@ import {codexExecutable} from './codex-account-client.mjs';
6
6
  import {VERSION} from './product.mjs';
7
7
 
8
8
  export async function diagnoseRuntime(){
9
- const version=process.versions.node.split('.').map(Number),checks=[{name:'Node.js',ok:version[0]>24||version[0]===24&&(version[1]>22||version[1]===22&&version[2]>=1),version:process.versions.node},{name:'Codex Unlimit',ok:true,version:VERSION}];
9
+ const version=process.versions.node.split('.').map(Number),checks=[{name:'Node.js',ok:version[0]===22&&version[1]>=15||version[0]>=24,version:process.versions.node},{name:'Codex Unlimit',ok:true,version:VERSION}];
10
10
  try{await import('node:sqlite');checks.push({name:'SQLite',ok:true});}catch{checks.push({name:'SQLite',ok:false});}
11
11
  const home=fs.mkdtempSync(path.join(os.tmpdir(),'cu-runtime-check-'));
12
12
  try{
@@ -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,13 +1,13 @@
1
1
  {
2
2
  "name": "codexunlimit",
3
- "version": "0.1.0-web.7",
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",
7
7
  "license": "UNLICENSED",
8
8
  "homepage": "https://codexunlimit.tudonghoa.me",
9
9
  "publishConfig": {"registry":"https://registry.npmjs.org","access":"public"},
10
- "engines": {"node": ">=24.22.1"},
10
+ "engines": {"node": "^22.15.0 || >=24.0.0"},
11
11
  "bin": {"codexunlimit": "cli.mjs"},
12
12
  "dependencies": {"@openai/codex":"0.166.0","markdown-it":"15.0.3"},
13
13
  "files": ["app.mjs","account-pool.mjs","codex-account-client.mjs","quota.mjs","cli.mjs","config.mjs","device.ps1","doctor.mjs","image-http.mjs","license-client.mjs","io.mjs","product.mjs","protocol.mjs","relay.mjs","state.mjs","windows.mjs","windows.ps1","web/","README.md"]
package/product.mjs CHANGED
@@ -1,4 +1,4 @@
1
- export const VERSION='0.1.0-web.7';
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ị.',