orbit-codex-bridge 0.1.6 → 0.1.8
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 +30 -2
- package/cli.mjs +27 -5
- package/lib/approvals.mjs +97 -0
- package/lib/client-files.mjs +37 -0
- package/lib/connector-service.mjs +13 -4
- package/lib/desktop.mjs +18 -24
- package/lib/resume-manager.mjs +39 -7
- package/lib/rpc.mjs +40 -21
- package/lib/store.mjs +6 -0
- package/lib/updates.mjs +97 -0
- package/package.json +19 -18
- package/relay-agent.mjs +11 -6
- package/scripts/verify-client-package.mjs +34 -0
- package/server.mjs +69 -126
- package/lib/device-auth.mjs +0 -69
- package/public/admin.css +0 -67
- package/public/admin.html +0 -23
- package/public/admin.js +0 -186
- package/public/app.js +0 -1382
- package/public/connect.html +0 -56
- package/public/connect.js +0 -126
- package/public/goal.css +0 -7
- package/public/icon.svg +0 -1
- package/public/index.html +0 -60
- package/public/layout.css +0 -73
- package/public/manifest.webmanifest +0 -1
- package/public/style.css +0 -70
- package/relay-server.mjs +0 -703
- package/relay-store.mjs +0 -368
package/lib/updates.mjs
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
const registryUrl = 'https://registry.npmjs.org/orbit-codex-bridge/latest';
|
|
2
|
+
const versionPattern = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][a-zA-Z0-9-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][a-zA-Z0-9-]*))*))?(?:\+[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*)?$/;
|
|
3
|
+
|
|
4
|
+
function parseVersion(value) {
|
|
5
|
+
if (typeof value !== 'string' || value.length > 120) throw new Error('版本格式无效。');
|
|
6
|
+
const match = value.match(versionPattern);
|
|
7
|
+
if (!match) throw new Error('版本格式无效。');
|
|
8
|
+
return { core:match.slice(1, 4).map(part => BigInt(part)), prerelease:match[4]?.split('.') || [] };
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function compareVersions(left, right) {
|
|
12
|
+
const first = parseVersion(left);
|
|
13
|
+
const second = parseVersion(right);
|
|
14
|
+
for (let index = 0; index < 3; index++) {
|
|
15
|
+
if (first.core[index] !== second.core[index]) return first.core[index] > second.core[index] ? 1 : -1;
|
|
16
|
+
}
|
|
17
|
+
if (!first.prerelease.length || !second.prerelease.length) {
|
|
18
|
+
return first.prerelease.length === second.prerelease.length ? 0 : first.prerelease.length ? -1 : 1;
|
|
19
|
+
}
|
|
20
|
+
for (let index = 0; index < Math.max(first.prerelease.length, second.prerelease.length); index++) {
|
|
21
|
+
const firstPart = first.prerelease[index];
|
|
22
|
+
const secondPart = second.prerelease[index];
|
|
23
|
+
if (firstPart === secondPart) continue;
|
|
24
|
+
if (firstPart === undefined || secondPart === undefined) return firstPart === undefined ? -1 : 1;
|
|
25
|
+
const firstNumeric = /^\d+$/.test(firstPart);
|
|
26
|
+
const secondNumeric = /^\d+$/.test(secondPart);
|
|
27
|
+
if (firstNumeric !== secondNumeric) return firstNumeric ? -1 : 1;
|
|
28
|
+
return firstNumeric ? BigInt(firstPart) > BigInt(secondPart) ? 1 : -1 : firstPart > secondPart ? 1 : -1;
|
|
29
|
+
}
|
|
30
|
+
return 0;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function updateCommand(platform = process.platform) {
|
|
34
|
+
return `${platform === 'win32' ? 'npx.cmd' : 'npx'} --yes --package=orbit-codex-bridge@latest orbit login`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function readMetadata(response) {
|
|
38
|
+
if (!response.ok || !response.body) throw new Error('更新服务暂不可用。');
|
|
39
|
+
const reader = response.body.getReader();
|
|
40
|
+
const chunks = [];
|
|
41
|
+
let bytes = 0;
|
|
42
|
+
try {
|
|
43
|
+
while (true) {
|
|
44
|
+
const { done, value } = await reader.read();
|
|
45
|
+
if (done) break;
|
|
46
|
+
bytes += value.byteLength;
|
|
47
|
+
if (bytes > 256 * 1024) throw new Error('版本信息过大。');
|
|
48
|
+
chunks.push(Buffer.from(value));
|
|
49
|
+
}
|
|
50
|
+
return JSON.parse(Buffer.concat(chunks).toString('utf8'));
|
|
51
|
+
} finally { await reader.cancel().catch(() => {}); }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function createUpdateChecker(currentVersion, {
|
|
55
|
+
fetchImpl = globalThis.fetch,
|
|
56
|
+
now = Date.now,
|
|
57
|
+
platform = process.platform,
|
|
58
|
+
timeout = 5000,
|
|
59
|
+
cacheLifetime = 6 * 60 * 60 * 1000,
|
|
60
|
+
failureLifetime = 5 * 60 * 1000,
|
|
61
|
+
minimumInterval = 60000
|
|
62
|
+
} = {}) {
|
|
63
|
+
parseVersion(currentVersion);
|
|
64
|
+
let result = { currentVersion, latestVersion:null, available:false, status:'unchecked', checkedAt:null, command:updateCommand(platform) };
|
|
65
|
+
let inFlight = null;
|
|
66
|
+
const snapshot = () => ({ ...result });
|
|
67
|
+
async function check({ force = false } = {}) {
|
|
68
|
+
if (inFlight) return inFlight;
|
|
69
|
+
const age = result.checkedAt === null ? Infinity : Math.max(0, now() - result.checkedAt);
|
|
70
|
+
const lifetime = force ? minimumInterval : result.status === 'unavailable' ? failureLifetime : cacheLifetime;
|
|
71
|
+
if (age < lifetime) return snapshot();
|
|
72
|
+
inFlight = (async () => {
|
|
73
|
+
try {
|
|
74
|
+
const response = await fetchImpl(registryUrl, {
|
|
75
|
+
headers:{ Accept:'application/json' }, signal:AbortSignal.timeout(timeout), redirect:'error', cache:'no-store'
|
|
76
|
+
});
|
|
77
|
+
const metadata = await readMetadata(response);
|
|
78
|
+
if (metadata.name !== 'orbit-codex-bridge') throw new Error('包信息不一致。');
|
|
79
|
+
const comparison = compareVersions(metadata.version, currentVersion);
|
|
80
|
+
result = {
|
|
81
|
+
currentVersion, latestVersion:metadata.version, available:comparison > 0,
|
|
82
|
+
status:comparison > 0 ? 'update-available' : comparison < 0 ? 'ahead' : 'up-to-date',
|
|
83
|
+
checkedAt:now(), command:updateCommand(platform)
|
|
84
|
+
};
|
|
85
|
+
} catch {
|
|
86
|
+
result = {
|
|
87
|
+
currentVersion, latestVersion:null, available:false, status:'unavailable', checkedAt:now(),
|
|
88
|
+
command:updateCommand(platform), message:'暂时无法检查更新,请稍后重试。'
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
return snapshot();
|
|
92
|
+
})();
|
|
93
|
+
try { return await inFlight; }
|
|
94
|
+
finally { inFlight = null; }
|
|
95
|
+
}
|
|
96
|
+
return { check, snapshot };
|
|
97
|
+
}
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "orbit-codex-bridge",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.8",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "Connect your original Codex
|
|
5
|
+
"description": "Connect your computer and original Codex conversations to Orbit.",
|
|
6
6
|
"engines": {
|
|
7
7
|
"node": ">=24"
|
|
8
8
|
},
|
|
@@ -14,25 +14,26 @@
|
|
|
14
14
|
"orbit",
|
|
15
15
|
"server.mjs",
|
|
16
16
|
"relay-agent.mjs",
|
|
17
|
-
"
|
|
18
|
-
"
|
|
19
|
-
"lib/",
|
|
20
|
-
"
|
|
17
|
+
"lib/approvals.mjs",
|
|
18
|
+
"lib/client-files.mjs",
|
|
19
|
+
"lib/connector-service.mjs",
|
|
20
|
+
"lib/desktop.mjs",
|
|
21
|
+
"lib/resume-manager.mjs",
|
|
22
|
+
"lib/rpc.mjs",
|
|
23
|
+
"lib/store.mjs",
|
|
24
|
+
"lib/updates.mjs",
|
|
25
|
+
"scripts/verify-client-package.mjs"
|
|
21
26
|
],
|
|
22
27
|
"scripts": {
|
|
23
|
-
"start": "node
|
|
24
|
-
"
|
|
25
|
-
"relay:agent": "node --env-file-if-exists=.env.local relay-agent.mjs",
|
|
26
|
-
"prepublishOnly": "npm run check && npm test",
|
|
27
|
-
"test": "node --test test/*.test.mjs",
|
|
28
|
-
"test:browser": "node --test test/browser/*.test.mjs",
|
|
29
|
-
"test:native": "node test/native-smoke.mjs",
|
|
30
|
-
"check": "node --check cli.mjs && node --check server.mjs && node --check lib/store.mjs && node --check lib/rpc.mjs && node --check lib/desktop.mjs && node --check lib/resume-manager.mjs && node --check lib/device-auth.mjs && node --check lib/connector-service.mjs && node --check public/app.js && node --check public/connect.js && node --check relay-server.mjs && node --check relay-store.mjs && node --check relay-agent.mjs && node --check public/admin.js"
|
|
28
|
+
"start": "node server.mjs",
|
|
29
|
+
"prepublishOnly": "node scripts/verify-client-package.mjs"
|
|
31
30
|
},
|
|
32
31
|
"dependencies": {
|
|
33
|
-
"dompurify": "^3.3.0",
|
|
34
|
-
"marked": "^17.0.0",
|
|
35
|
-
"qrcode": "^1.5.4",
|
|
36
32
|
"ws": "^8.18.3"
|
|
37
|
-
}
|
|
33
|
+
},
|
|
34
|
+
"publishConfig": {
|
|
35
|
+
"registry": "https://registry.npmjs.org/",
|
|
36
|
+
"access": "public"
|
|
37
|
+
},
|
|
38
|
+
"orbitDistribution": "client"
|
|
38
39
|
}
|
package/relay-agent.mjs
CHANGED
|
@@ -69,9 +69,7 @@ export function createRelayAgent(options = {}) {
|
|
|
69
69
|
requests.delete(request);
|
|
70
70
|
resolve({statusCode:response.statusCode || 502,headers:{
|
|
71
71
|
'content-type':response.headers['content-type'] || 'application/json',
|
|
72
|
-
...(response.headers['set-cookie'] ? {'set-cookie':response.headers['set-cookie']} : {})
|
|
73
|
-
...Object.fromEntries(['content-disposition','content-length','accept-ranges','content-range','etag']
|
|
74
|
-
.filter(name=>typeof response.headers[name]==='string').map(name=>[name,response.headers[name]]))
|
|
72
|
+
...(response.headers['set-cookie'] ? {'set-cookie':response.headers['set-cookie']} : {})
|
|
75
73
|
},body:Buffer.concat(chunks)});
|
|
76
74
|
});
|
|
77
75
|
});
|
|
@@ -99,7 +97,7 @@ export function createRelayAgent(options = {}) {
|
|
|
99
97
|
const body = Buffer.from(message.body || '', 'base64');
|
|
100
98
|
if (body.length > maxBody) return;
|
|
101
99
|
const headers = {};
|
|
102
|
-
for (const name of ['accept','content-type','cookie','user-agent'
|
|
100
|
+
for (const name of ['accept','content-type','cookie','user-agent']) {
|
|
103
101
|
const value = message.headers?.[name];
|
|
104
102
|
if (typeof value === 'string') headers[name] = value;
|
|
105
103
|
}
|
|
@@ -228,8 +226,15 @@ export function createRelayAgent(options = {}) {
|
|
|
228
226
|
if (!stopped) {
|
|
229
227
|
if (code === 1008 || code === 1012 && reason.toString() === 'Replaced by a newer agent') {
|
|
230
228
|
stopped = true;
|
|
231
|
-
const
|
|
232
|
-
|
|
229
|
+
const replaced = code === 1012;
|
|
230
|
+
const authenticationFailed = code === 1008 && reason.toString() === 'Agent authentication failed';
|
|
231
|
+
const reasonCode = replaced ? 'AGENT_REPLACED' : authenticationFailed ? 'DEVICE_AUTH_REJECTED' : 'AGENT_ACCESS_DENIED';
|
|
232
|
+
const detail = replaced
|
|
233
|
+
? '连接已由另一个 Orbit 实例接管。请只保留一个连接器实例;确认后运行 orbit restart。'
|
|
234
|
+
: authenticationFailed
|
|
235
|
+
? '云端拒绝了此电脑的连接凭证。请确认账号有效、电脑绑定未撤销;需要重新绑定时运行 orbit rebind。'
|
|
236
|
+
: '云端拒绝了此电脑的访问。请检查账号状态、有效期及电脑绑定;恢复权限后运行 orbit restart。';
|
|
237
|
+
onState({status:'blocked',reason:detail,reasonCode});
|
|
233
238
|
onError(new Error(detail));
|
|
234
239
|
return;
|
|
235
240
|
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { isDeepStrictEqual } from 'node:util';
|
|
5
|
+
import { clientFiles, clientManifest } from '../lib/client-files.mjs';
|
|
6
|
+
|
|
7
|
+
export function verifyClientPackage(directory) {
|
|
8
|
+
const root=fs.realpathSync(directory);
|
|
9
|
+
const manifest=JSON.parse(fs.readFileSync(path.join(root,'package.json'),'utf8'));
|
|
10
|
+
if (!isDeepStrictEqual(manifest,clientManifest(manifest))) throw new Error('客户端 package.json 不符合发布白名单。');
|
|
11
|
+
const allowed=new Set(['package.json','package-lock.json','README.md',...clientFiles]);
|
|
12
|
+
const found=new Set();
|
|
13
|
+
const visit=(current,prefix='')=>{
|
|
14
|
+
for(const entry of fs.readdirSync(current,{withFileTypes:true})){
|
|
15
|
+
if(!prefix&&entry.name==='node_modules'){
|
|
16
|
+
if(!entry.isDirectory()||entry.isSymbolicLink())throw new Error('node_modules 必须是普通目录。');
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
const relative=prefix+entry.name;
|
|
20
|
+
if(entry.isSymbolicLink())throw new Error(`客户端发布目录不允许符号链接:${relative}`);
|
|
21
|
+
if(entry.isDirectory())visit(path.join(current,entry.name),`${relative}/`);
|
|
22
|
+
else if(entry.isFile()&&allowed.has(relative))found.add(relative);
|
|
23
|
+
else throw new Error(`客户端包含非白名单文件:${relative}`);
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
visit(root);
|
|
27
|
+
for(const filename of ['package.json','README.md',...clientFiles])if(!found.has(filename))throw new Error(`客户端缺少文件:${filename}`);
|
|
28
|
+
return {version:manifest.version,files:[...found].sort()};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if(process.argv[1]&&path.resolve(process.argv[1])===fileURLToPath(import.meta.url)){
|
|
32
|
+
const result=verifyClientPackage(path.resolve(path.dirname(fileURLToPath(import.meta.url)),'..'));
|
|
33
|
+
console.log(`Client package verified: ${result.version} · ${result.files.length} files`);
|
|
34
|
+
}
|
package/server.mjs
CHANGED
|
@@ -5,14 +5,15 @@ import os from 'node:os';
|
|
|
5
5
|
import crypto from 'node:crypto';
|
|
6
6
|
import { fileURLToPath } from 'node:url';
|
|
7
7
|
import { WebSocketServer, WebSocket } from 'ws';
|
|
8
|
-
import QRCode from 'qrcode';
|
|
9
8
|
import { NativeStore } from './lib/store.mjs';
|
|
10
9
|
import { CodexRpc } from './lib/rpc.mjs';
|
|
11
10
|
import { DesktopRpc } from './lib/desktop.mjs';
|
|
12
11
|
import { ResumeManager } from './lib/resume-manager.mjs';
|
|
12
|
+
import { createUpdateChecker } from './lib/updates.mjs';
|
|
13
13
|
|
|
14
14
|
const root = path.dirname(fileURLToPath(import.meta.url));
|
|
15
|
-
const
|
|
15
|
+
const packageManifest = JSON.parse(fs.readFileSync(path.join(root,'package.json'),'utf8'));
|
|
16
|
+
const packageVersion = packageManifest.version;
|
|
16
17
|
const sessionLifetime = 12 * 60 * 60 * 1000;
|
|
17
18
|
const attachmentLifetime = 30 * 60 * 1000;
|
|
18
19
|
const attachmentRetention = 24 * 60 * 60 * 1000;
|
|
@@ -47,107 +48,9 @@ function cleanupStaleAttachments(store) {
|
|
|
47
48
|
}
|
|
48
49
|
}
|
|
49
50
|
|
|
50
|
-
const excludedProjectEntries=new Set([
|
|
51
|
-
'.git','.codex','.orbit-attachments','node_modules','.venv','venv','dist','build',
|
|
52
|
-
'.ssh','.aws','.azure','.config','.npm','.docker','secrets','credentials'
|
|
53
|
-
]);
|
|
54
|
-
const excludedProjectFiles=/^(?:\.env(?:\..*)?|\.npmrc|\.pypirc|\.netrc|\.git-credentials|id_(?:rsa|ed25519)(?:\.pub)?|.*(?:secret|credential)(?:\..*)?|.*\.(?:pem|p12|pfx|key|keystore))$/i;
|
|
55
|
-
|
|
56
|
-
function checkedProjectEntry(row,relativePath=''){
|
|
57
|
-
if(!row?.cwd||typeof relativePath!=='string'||relativePath.length>1024||relativePath.includes('\0'))throw new Error('项目文件路径无效。');
|
|
58
|
-
const root=fs.realpathSync(row.cwd);
|
|
59
|
-
if(!fs.statSync(root).isDirectory())throw new Error('会话工作目录不可用。');
|
|
60
|
-
const parts=relativePath?relativePath.split(/[\\/]/):[];
|
|
61
|
-
if(parts.some(part=>!part||part==='.'||part==='..'))throw new Error('不允许访问项目目录之外的路径。');
|
|
62
|
-
let current=root;
|
|
63
|
-
for(const part of parts){
|
|
64
|
-
if(excludedProjectEntries.has(part.toLowerCase())||excludedProjectFiles.test(part))throw new Error('此系统目录或凭证文件不开放传输。');
|
|
65
|
-
current=path.join(current,part);
|
|
66
|
-
const stat=fs.lstatSync(current);
|
|
67
|
-
if(stat.isSymbolicLink())throw new Error('不支持通过符号链接传输文件。');
|
|
68
|
-
}
|
|
69
|
-
const resolved=path.resolve(current);
|
|
70
|
-
const relative=path.relative(root,resolved);
|
|
71
|
-
if(relative==='..'||relative.startsWith(`..${path.sep}`)||path.isAbsolute(relative))throw new Error('不允许访问项目目录之外的路径。');
|
|
72
|
-
return {root,path:resolved,stat:fs.statSync(resolved),relative:parts.join(path.sep)};
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function projectFileList(row,relativePath=''){
|
|
76
|
-
const directory=checkedProjectEntry(row,relativePath);
|
|
77
|
-
if(!directory.stat.isDirectory())throw new Error('所选路径不是文件夹。');
|
|
78
|
-
const entries=[];
|
|
79
|
-
const visit=(current,relative,depth)=>{
|
|
80
|
-
if(depth>8||entries.length>=1000)return;
|
|
81
|
-
for(const name of fs.readdirSync(current)){
|
|
82
|
-
if(excludedProjectEntries.has(name.toLowerCase())||excludedProjectFiles.test(name))continue;
|
|
83
|
-
const target=path.join(current,name);
|
|
84
|
-
let stat;
|
|
85
|
-
try{stat=fs.lstatSync(target)}catch{continue}
|
|
86
|
-
if(stat.isSymbolicLink())continue;
|
|
87
|
-
const child=relative?`${relative}/${name}`:name;
|
|
88
|
-
if(stat.isDirectory())entries.push({name,path:child,type:'directory',size:null,updatedAt:stat.mtimeMs});
|
|
89
|
-
else if(stat.isFile())entries.push({name,path:child,type:'file',size:stat.size,updatedAt:stat.mtimeMs});
|
|
90
|
-
if(stat.isDirectory())visit(target,child,depth+1);
|
|
91
|
-
if(entries.length>=1000)break;
|
|
92
|
-
}
|
|
93
|
-
};
|
|
94
|
-
visit(directory.path,directory.relative,0);
|
|
95
|
-
entries.sort((left,right)=>left.type===right.type?left.name.localeCompare(right.name):left.type==='directory'?-1:1);
|
|
96
|
-
return {path:directory.relative,entries:entries.slice(0,1000),truncated:entries.length>=1000};
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
const projectMimeTypes=new Map([
|
|
100
|
-
['.txt','text/plain; charset=utf-8'],['.md','text/markdown; charset=utf-8'],['.json','application/json'],
|
|
101
|
-
['.csv','text/csv; charset=utf-8'],['.html','text/html; charset=utf-8'],['.css','text/css; charset=utf-8'],
|
|
102
|
-
['.js','text/javascript; charset=utf-8'],['.ts','text/plain; charset=utf-8'],['.xml','application/xml'],
|
|
103
|
-
['.pdf','application/pdf'],['.png','image/png'],['.jpg','image/jpeg'],['.jpeg','image/jpeg'],['.gif','image/gif'],
|
|
104
|
-
['.webp','image/webp'],['.svg','image/svg+xml'],['.zip','application/zip']
|
|
105
|
-
]);
|
|
106
|
-
|
|
107
|
-
function sendFileRange(request,response,row,relativePath){
|
|
108
|
-
const file=checkedProjectEntry(row,relativePath);
|
|
109
|
-
if(!file.stat.isFile())throw new Error('所选路径不是文件。');
|
|
110
|
-
if(file.stat.size>8*1024*1024)throw Object.assign(new Error('单个文件下载不得超过 8 MiB。'),{status:413});
|
|
111
|
-
const filename=path.basename(file.path);
|
|
112
|
-
const fallback=filename.replace(/[^\x20-\x7e]|["\\;]/g,'_');
|
|
113
|
-
const encoded=encodeURIComponent(filename).replace(/[!'()*]/g,character=>`%${character.charCodeAt(0).toString(16).toUpperCase()}`);
|
|
114
|
-
const etag=`"orbit-${file.stat.size}-${Math.trunc(file.stat.mtimeMs)}"`;
|
|
115
|
-
const headers={
|
|
116
|
-
'Content-Type':projectMimeTypes.get(path.extname(filename).toLowerCase())||'application/octet-stream',
|
|
117
|
-
'Accept-Ranges':'bytes',
|
|
118
|
-
'ETag':etag,
|
|
119
|
-
'Content-Disposition':`attachment; filename="${fallback}"; filename*=UTF-8''${encoded}`,
|
|
120
|
-
'X-Content-Type-Options':'nosniff'
|
|
121
|
-
};
|
|
122
|
-
if(request.headers['if-none-match']===etag){response.writeHead(304,headers);return response.end()}
|
|
123
|
-
const ifRange=request.headers['if-range'];
|
|
124
|
-
const range=ifRange&&ifRange!==etag?null:request.headers.range;
|
|
125
|
-
if(!range){
|
|
126
|
-
response.writeHead(200,{...headers,'Content-Length':file.stat.size});
|
|
127
|
-
if(request.method==='HEAD')return response.end();
|
|
128
|
-
return fs.createReadStream(file.path).pipe(response);
|
|
129
|
-
}
|
|
130
|
-
const match=String(range).match(/^bytes=(\d*)-(\d*)$/);
|
|
131
|
-
if(!match||(!match[1]&&!match[2])){response.writeHead(416,{...headers,'Content-Range':`bytes */${file.stat.size}`});return response.end()}
|
|
132
|
-
let start,end;
|
|
133
|
-
if(!match[1]){
|
|
134
|
-
const suffix=Number(match[2]);
|
|
135
|
-
if(!Number.isSafeInteger(suffix)||suffix<=0)return response.writeHead(416,{'Content-Range':`bytes */${file.stat.size}`}).end();
|
|
136
|
-
start=Math.max(0,file.stat.size-suffix);end=file.stat.size-1;
|
|
137
|
-
}else{
|
|
138
|
-
start=Number(match[1]);end=match[2]?Number(match[2]):file.stat.size-1;
|
|
139
|
-
}
|
|
140
|
-
if(!Number.isSafeInteger(start)||!Number.isSafeInteger(end)||start<0||start>=file.stat.size||end<start){
|
|
141
|
-
response.writeHead(416,{...headers,'Content-Range':`bytes */${file.stat.size}`});return response.end();
|
|
142
|
-
}
|
|
143
|
-
end=Math.min(end,file.stat.size-1);
|
|
144
|
-
const length=end-start+1;
|
|
145
|
-
response.writeHead(206,{...headers,'Content-Length':length,'Content-Range':`bytes ${start}-${end}/${file.stat.size}`});
|
|
146
|
-
if(request.method==='HEAD')return response.end();
|
|
147
|
-
fs.createReadStream(file.path,{start,end}).pipe(response);
|
|
148
|
-
}
|
|
149
|
-
|
|
150
51
|
export function createOrbitServer(options = {}) {
|
|
52
|
+
const clientOnly=options.clientOnly ?? packageManifest.orbitDistribution==='client';
|
|
53
|
+
const updateChecker=options.updateChecker || createUpdateChecker(packageVersion);
|
|
151
54
|
const home = options.home || process.env.ORBIT_CODEX_HOME || process.env.CODEX_HOME || path.join(os.homedir(), '.codex');
|
|
152
55
|
const dataDir = options.dataDir || path.join(root, '.runtime');
|
|
153
56
|
fs.mkdirSync(dataDir, { recursive: true, mode: 0o700 });
|
|
@@ -173,7 +76,7 @@ export function createOrbitServer(options = {}) {
|
|
|
173
76
|
}
|
|
174
77
|
async function attachThread(id) {
|
|
175
78
|
if (executionMode !== 'desktop') return null;
|
|
176
|
-
if (resumeManager?.has(id)) return resumeManager.
|
|
79
|
+
if (resumeManager?.has(id)) return resumeManager.resume(id);
|
|
177
80
|
try { return await rpc.attach(id); }
|
|
178
81
|
catch (error) {
|
|
179
82
|
const row = store.row(id);
|
|
@@ -215,6 +118,21 @@ export function createOrbitServer(options = {}) {
|
|
|
215
118
|
}).finally(() => { modelLoading = null; });
|
|
216
119
|
return modelLoading;
|
|
217
120
|
}
|
|
121
|
+
function workspaceCatalog() {
|
|
122
|
+
const workspaces=new Map();
|
|
123
|
+
workspaces.set('personal',{id:'personal',cwd:path.join(dataDir,'workspaces','personal'),projectId:null,label:'个人会话 · 不绑定项目',isDefault:true});
|
|
124
|
+
const add=(directory,label,projectId=null)=>{
|
|
125
|
+
try{
|
|
126
|
+
const resolved=fs.realpathSync(directory);
|
|
127
|
+
if(!fs.statSync(resolved).isDirectory())return;
|
|
128
|
+
const key=`${projectId||''}:${process.platform==='win32'?resolved.toLowerCase():resolved}`;
|
|
129
|
+
if(!workspaces.has(key))workspaces.set(key,{id:key,cwd:resolved,label,projectId,isDefault:false});
|
|
130
|
+
}catch{}
|
|
131
|
+
};
|
|
132
|
+
for(const workspace of store.projectWorkspaces?.()||[])add(workspace.cwd,workspace.label,workspace.projectId);
|
|
133
|
+
for(const thread of store.list())if(thread.cwd)add(thread.cwd,thread.projectName||path.basename(thread.cwd),thread.nativeProjectId||null);
|
|
134
|
+
return [...workspaces.values()].sort((left,right)=>Number(right.isDefault)-Number(left.isDefault)||left.label.localeCompare(right.label));
|
|
135
|
+
}
|
|
218
136
|
const publicUrl = options.publicUrl || process.env.ORBIT_PUBLIC_URL || '';
|
|
219
137
|
if (publicUrl && new URL(publicUrl).protocol !== 'https:') throw new Error('ORBIT_PUBLIC_URL 必须使用 HTTPS。');
|
|
220
138
|
const host = options.host || process.env.ORBIT_HOST || '127.0.0.1';
|
|
@@ -229,6 +147,7 @@ export function createOrbitServer(options = {}) {
|
|
|
229
147
|
let port = Number(options.port ?? process.env.ORBIT_PORT ?? 4318);
|
|
230
148
|
let revision = store.revision();
|
|
231
149
|
let timer;
|
|
150
|
+
let updateTimer;
|
|
232
151
|
let lastDesktopConnect = 0;
|
|
233
152
|
let lastAttachmentCleanup=Date.now();
|
|
234
153
|
|
|
@@ -333,7 +252,7 @@ export function createOrbitServer(options = {}) {
|
|
|
333
252
|
const resumedIds = resumeManager?.ids() || [];
|
|
334
253
|
for (const id of session.enabled.keys()) controlFor(session, id);
|
|
335
254
|
return {
|
|
336
|
-
product: 'Orbit', version: packageVersion, device: os.hostname(), platform: process.platform, home,
|
|
255
|
+
product: 'Orbit', version: packageVersion, updates:updateChecker.snapshot(), device: os.hostname(), platform: process.platform, home,
|
|
337
256
|
threadCount: threads.length, projectCount: new Set(threads.map(thread => thread.projectId)).size,
|
|
338
257
|
archivedCount: threads.filter(thread => thread.archived).length, lockedCount: threads.filter(thread => thread.locked).length,
|
|
339
258
|
bridge: 'connected', transport: 'websocket', readOnlyMirror: true,
|
|
@@ -447,7 +366,9 @@ export function createOrbitServer(options = {}) {
|
|
|
447
366
|
const session = sessionFor(request);
|
|
448
367
|
if (!session) return json(response, 401, { error: '请先配对这台电脑。' });
|
|
449
368
|
if (request.method === 'GET' && url.pathname === '/api/status') return json(response, 200, status(session));
|
|
369
|
+
if (request.method === 'GET' && url.pathname === '/api/updates') return json(response, 200, await updateChecker.check({force:true}));
|
|
450
370
|
if (request.method === 'GET' && url.pathname === '/api/threads') return json(response, 200, { threads: store.list(), revision });
|
|
371
|
+
if (request.method === 'GET' && url.pathname === '/api/workspaces') return json(response, 200, { workspaces:workspaceCatalog() });
|
|
451
372
|
if (request.method === 'GET' && url.pathname === '/api/models') return json(response, 200, { models: await modelCatalog() });
|
|
452
373
|
if (request.method === 'POST' && url.pathname === '/api/logout') {
|
|
453
374
|
sessions.delete(session.key);
|
|
@@ -456,9 +377,11 @@ export function createOrbitServer(options = {}) {
|
|
|
456
377
|
return json(response, 200, { ok: true }, { 'Set-Cookie': 'orbit_session=; HttpOnly; SameSite=Strict; Path=/; Max-Age=0' });
|
|
457
378
|
}
|
|
458
379
|
if (request.method === 'POST' && url.pathname === '/api/pair/qr') {
|
|
380
|
+
if(clientOnly)return json(response,404,{error:'电脑连接器不提供网页配对,请在工作台登录账号。'});
|
|
459
381
|
const body = await readBody(request);
|
|
460
382
|
const connection = status(session).connectionUrls.find(value => value === body.url);
|
|
461
383
|
if (!connection) return json(response, 400, { error: '请选择桥接服务提供的连接地址。' });
|
|
384
|
+
const {default:QRCode}=await import('qrcode');
|
|
462
385
|
return json(response, 200, { image: await QRCode.toDataURL(`${connection}/#pair=${encodeURIComponent(pairingKey)}`, { width: 280, margin: 2 }), warning: '扫码者将能查看本机会话。仅向可信设备展示。' });
|
|
463
386
|
}
|
|
464
387
|
const uploadMatch=url.pathname.match(/^\/api\/threads\/([a-zA-Z0-9_-]{8,120})\/attachments$/);
|
|
@@ -472,18 +395,6 @@ export function createOrbitServer(options = {}) {
|
|
|
472
395
|
removeAttachment(session,body.attachmentId);
|
|
473
396
|
return json(response,200,{removed:true});
|
|
474
397
|
}
|
|
475
|
-
const filesMatch=url.pathname.match(/^\/api\/threads\/([a-zA-Z0-9_-]{8,120})\/files$/);
|
|
476
|
-
if(request.method==='GET'&&filesMatch){
|
|
477
|
-
const row=store.row(filesMatch[1]);
|
|
478
|
-
if(!row)return json(response,404,{error:'会话不存在。'});
|
|
479
|
-
return json(response,200,projectFileList(row,url.searchParams.get('path')||''));
|
|
480
|
-
}
|
|
481
|
-
const fileMatch=url.pathname.match(/^\/api\/threads\/([a-zA-Z0-9_-]{8,120})\/file$/);
|
|
482
|
-
if(request.method==='GET'&&fileMatch){
|
|
483
|
-
const row=store.row(fileMatch[1]);
|
|
484
|
-
if(!row)return json(response,404,{error:'会话不存在。'});
|
|
485
|
-
return sendFileRange(request,response,row,url.searchParams.get('path')||'');
|
|
486
|
-
}
|
|
487
398
|
const match = url.pathname.match(/^\/api\/threads\/([a-zA-Z0-9_-]{8,120})\/messages$/);
|
|
488
399
|
if (request.method === 'GET' && match) {
|
|
489
400
|
const history = store.historyFor(match[1], url.searchParams.get('before'), url.searchParams.get('limit') || 60);
|
|
@@ -491,6 +402,7 @@ export function createOrbitServer(options = {}) {
|
|
|
491
402
|
}
|
|
492
403
|
return json(response, 404, { error: '接口不存在。' });
|
|
493
404
|
}
|
|
405
|
+
if(clientOnly)return json(response,404,{error:'这是 Orbit 电脑连接器。请在 Orbit 工作台登录并选择这台电脑。'});
|
|
494
406
|
if (request.method !== 'GET' && request.method !== 'HEAD') return json(response, 405, { error: '不支持的方法。' });
|
|
495
407
|
let file;
|
|
496
408
|
let type;
|
|
@@ -548,9 +460,34 @@ export function createOrbitServer(options = {}) {
|
|
|
548
460
|
async function command(socket, session, message) {
|
|
549
461
|
const body = message.params || {};
|
|
550
462
|
const id = body.threadId;
|
|
463
|
+
if (message.method === 'thread.create') {
|
|
464
|
+
if (executionMode !== 'desktop' || !resumeManager) throw new Error('当前执行通道不支持在本机 Codex 会话库创建会话。');
|
|
465
|
+
if (typeof body.text !== 'string' || !body.text.trim() || body.text.length > 20000) throw new Error('新会话的首条消息不能为空,且不得超过 20000 个字符。');
|
|
466
|
+
if (!['default','plan'].includes(body.mode || 'default')) throw new Error('新会话模式无效。');
|
|
467
|
+
const workspace=workspaceCatalog().find(entry=>entry.id===body.workspaceId);
|
|
468
|
+
if(!workspace)throw new Error('项目空间不可用,请刷新后重新选择。');
|
|
469
|
+
if(workspace.isDefault)fs.mkdirSync(workspace.cwd,{recursive:true,mode:0o700});
|
|
470
|
+
let model=null;
|
|
471
|
+
if(body.model!==undefined&&body.model!==null){
|
|
472
|
+
if(typeof body.model!=='string')throw new Error('模型选择无效。');
|
|
473
|
+
model=(await modelCatalog()).find(entry=>entry.model===body.model)?.model||null;
|
|
474
|
+
if(!model)throw new Error('所选模型不可用,请刷新模型列表。');
|
|
475
|
+
}
|
|
476
|
+
const created=await resumeManager.startNew({cwd:workspace.cwd,projectId:workspace.projectId,model,text:body.text.trim(),writable:true,mode:body.mode||'default',onCreated:threadId=>{
|
|
477
|
+
unwatchDesktop(socket);
|
|
478
|
+
session.enabled.set(threadId,{sharedExecution:false,desktop:false,writable:true,expires:Date.now()+1800000});
|
|
479
|
+
if(socket.readyState!==WebSocket.OPEN)return;
|
|
480
|
+
socket.subscription=threadId;
|
|
481
|
+
socket.desktopSubscription=threadId;
|
|
482
|
+
desktopWatchers.set(threadId,(desktopWatchers.get(threadId)||0)+1);
|
|
483
|
+
}});
|
|
484
|
+
if(!(desktopWatchers.get(created.threadId)>0))void resumeManager.release(created.threadId).catch(()=>{});
|
|
485
|
+
if(created.startError)created.startError=safeError(new Error(created.startError));
|
|
486
|
+
return created;
|
|
487
|
+
}
|
|
551
488
|
if (message.method === 'thread.subscribe') {
|
|
552
489
|
const row = id ? store.row(id) : null;
|
|
553
|
-
if (id && !row) throw new Error('会话不存在。');
|
|
490
|
+
if (id && !row && !resumeManager?.has(id)) throw new Error('会话不存在。');
|
|
554
491
|
if (socket.desktopSubscription && socket.desktopSubscription !== (id || null)) unwatchDesktop(socket);
|
|
555
492
|
socket.subscription = id || null;
|
|
556
493
|
let runtime = null;
|
|
@@ -573,13 +510,13 @@ export function createOrbitServer(options = {}) {
|
|
|
573
510
|
if (resumeManager?.has(id) && (desktopWatchers.get(id) || 0) === 0) void resumeManager.release(id).catch(() => {});
|
|
574
511
|
}
|
|
575
512
|
if (executionMode === 'desktop' && runtime?.attached && body.autoControl === true
|
|
576
|
-
&& !row
|
|
513
|
+
&& !row?.archived && !String(row?.source||'').startsWith('{')) {
|
|
577
514
|
const desktop = runtime.mode !== 'same-id-resume';
|
|
578
|
-
session.enabled.set(id, {sharedExecution:desktop,desktop,writable:!desktop,expires:Date.now() + 1800000});
|
|
515
|
+
session.enabled.set(id, {sharedExecution:desktop,desktop,writable:!desktop&&runtime.permissionMode!=='read-only',expires:Date.now() + 1800000});
|
|
579
516
|
}
|
|
580
517
|
const control = controlFor(session, id);
|
|
581
518
|
const activeRpc = resumeManager?.rpcFor(id) || rpc;
|
|
582
|
-
const approvals = control ? (executionMode === 'desktop' ? runtime?.approvals || [] : [...activeRpc.approvals.
|
|
519
|
+
const approvals = control ? (executionMode === 'desktop' ? runtime?.approvals || [] : [...activeRpc.approvals.entries()].filter(([,request]) => request.params.threadId === id).map(([requestId,request]) => ({ ...request.params, requestId, method: request.method }))) : [];
|
|
583
520
|
return { subscribed: id || null, control, runtime, settings: control ? session.preferences.get(id) || null : null, turnId: runtime?.turnId || activeRpc.threads.get(id)?.turnId || null, approvals, attachments:[...session.attachments.values()].filter(item=>item.threadId===id&&!item.consumed&&item.expires>Date.now()).map(({id,label,size,mimeType})=>({id,label,size,mimeType})) };
|
|
584
521
|
}
|
|
585
522
|
if (message.method === 'thread.enable') {
|
|
@@ -593,7 +530,7 @@ export function createOrbitServer(options = {}) {
|
|
|
593
530
|
catch (error) { lostListener(id,error.message,error.code); throw error; }
|
|
594
531
|
if (!runtime.attached) throw new Error('桌面实时快照未就绪,未授权控制。');
|
|
595
532
|
const desktop = runtime.mode !== 'same-id-resume';
|
|
596
|
-
const control = { sharedExecution:desktop, desktop, writable:!desktop, expires: Date.now() + 1800000 };
|
|
533
|
+
const control = { sharedExecution:desktop, desktop, writable:!desktop&&runtime.permissionMode!=='read-only', expires: Date.now() + 1800000 };
|
|
597
534
|
session.enabled.set(id, control);
|
|
598
535
|
return { threadId: id, ...control, mode:runtime.mode || 'desktop', turnId: runtime.turnId };
|
|
599
536
|
}
|
|
@@ -612,7 +549,8 @@ export function createOrbitServer(options = {}) {
|
|
|
612
549
|
if (!store.row(id)) throw new Error('会话不存在。');
|
|
613
550
|
if (!controlFor(session, id)) throw new Error('请先连接并接管这条会话。');
|
|
614
551
|
const runtime = runtimeForThread(id);
|
|
615
|
-
if (runtime?.mode === 'same-id-resume'
|
|
552
|
+
if (runtime?.mode === 'same-id-resume') return { goal:runtime.goal || null };
|
|
553
|
+
if (executionMode === 'desktop' && runtime?.attached) return { goal:runtime.goal || null };
|
|
616
554
|
if (rpc.threads?.has(id) && typeof rpc.request === 'function') return rpc.request('thread/goal/get', { threadId:id });
|
|
617
555
|
return { goal:null };
|
|
618
556
|
}
|
|
@@ -625,7 +563,7 @@ export function createOrbitServer(options = {}) {
|
|
|
625
563
|
if (tokenBudget !== null && (!Number.isSafeInteger(tokenBudget) || tokenBudget < 1 || tokenBudget > 10000000)) throw new Error('目标 token 预算必须在 1 至 10000000 之间。');
|
|
626
564
|
const runtime = runtimeForThread(id);
|
|
627
565
|
if (runtime?.mode === 'same-id-resume') return { goal:await resumeManager.setGoal(id, { objective:body.objective.trim(), tokenBudget }) };
|
|
628
|
-
if (executionMode === 'desktop' && runtime?.attached) throw new Error('
|
|
566
|
+
if (executionMode === 'desktop' && runtime?.attached) throw new Error('这条会话由 Codex 桌面持有,目标修改需要桌面 owner 支持。草稿可以继续编辑,当前未保存。');
|
|
629
567
|
if (rpc.threads?.has(id) && typeof rpc.request === 'function') return rpc.request('thread/goal/set', {
|
|
630
568
|
threadId:id, origin:'user', objective:body.objective.trim(), status:'active', tokenBudget
|
|
631
569
|
});
|
|
@@ -636,7 +574,7 @@ export function createOrbitServer(options = {}) {
|
|
|
636
574
|
if (!['active','paused','blocked','complete'].includes(body.status)) throw new Error('无效的目标状态。');
|
|
637
575
|
const runtime = runtimeForThread(id);
|
|
638
576
|
if (runtime?.mode === 'same-id-resume') return { goal:await resumeManager.setGoalStatus(id, body.status) };
|
|
639
|
-
if (executionMode === 'desktop' && runtime?.attached) throw new Error('
|
|
577
|
+
if (executionMode === 'desktop' && runtime?.attached) throw new Error('这条会话由 Codex 桌面持有,目标状态修改需要桌面 owner 支持。');
|
|
640
578
|
if (rpc.threads?.has(id) && typeof rpc.request === 'function') return rpc.request('thread/goal/set', { threadId:id, origin:'user', status:body.status });
|
|
641
579
|
throw new Error('当前执行通道不支持原生 Goal 状态更新。');
|
|
642
580
|
}
|
|
@@ -644,7 +582,7 @@ export function createOrbitServer(options = {}) {
|
|
|
644
582
|
if (!controlFor(session, id) || !store.row(id)) throw new Error('请先连接并接管这条会话。');
|
|
645
583
|
const runtime = runtimeForThread(id);
|
|
646
584
|
if (runtime?.mode === 'same-id-resume') return resumeManager.clearGoal(id);
|
|
647
|
-
if (executionMode === 'desktop' && runtime?.attached) throw new Error('
|
|
585
|
+
if (executionMode === 'desktop' && runtime?.attached) throw new Error('这条会话由 Codex 桌面持有,目标清除需要桌面 owner 支持。');
|
|
648
586
|
if (rpc.threads?.has(id) && typeof rpc.request === 'function') return rpc.request('thread/goal/clear', { threadId:id, origin:'user' });
|
|
649
587
|
throw new Error('当前执行通道不支持原生 Goal 清除。');
|
|
650
588
|
}
|
|
@@ -789,6 +727,11 @@ export function createOrbitServer(options = {}) {
|
|
|
789
727
|
async function listen() {
|
|
790
728
|
await new Promise((resolve, reject) => { server.once('error', reject); server.listen(port, host, resolve); });
|
|
791
729
|
port = server.address().port;
|
|
730
|
+
if(clientOnly){
|
|
731
|
+
void updateChecker.check();
|
|
732
|
+
updateTimer=setInterval(()=>{void updateChecker.check();},6*60*60*1000);
|
|
733
|
+
updateTimer.unref();
|
|
734
|
+
}
|
|
792
735
|
timer = setInterval(() => {
|
|
793
736
|
try {
|
|
794
737
|
tryDesktopConnect();
|
|
@@ -818,7 +761,7 @@ export function createOrbitServer(options = {}) {
|
|
|
818
761
|
}, 30000);
|
|
819
762
|
heartbeat.unref();
|
|
820
763
|
async function close() {
|
|
821
|
-
clearInterval(timer); clearInterval(heartbeat);
|
|
764
|
+
clearInterval(timer); clearInterval(heartbeat); clearInterval(updateTimer);
|
|
822
765
|
for (const socket of sockets) unwatchDesktop(socket);
|
|
823
766
|
for(const session of sessions.values())removeSessionAttachments(session);
|
|
824
767
|
for (const socket of sockets) socket.terminate();
|
package/lib/device-auth.mjs
DELETED
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
import crypto from 'node:crypto';
|
|
2
|
-
|
|
3
|
-
const digest = value => crypto.createHash('sha256').update(value).digest('hex');
|
|
4
|
-
|
|
5
|
-
export class DeviceAuthorization {
|
|
6
|
-
constructor({ lifetime = 5 * 60 * 1000, now = Date.now } = {}) {
|
|
7
|
-
this.lifetime = lifetime;
|
|
8
|
-
this.now = now;
|
|
9
|
-
this.pending = new Map();
|
|
10
|
-
this.codes = new Map();
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
cleanup() {
|
|
14
|
-
for (const [key, value] of this.pending) {
|
|
15
|
-
if (value.expiresAt > this.now()) continue;
|
|
16
|
-
this.pending.delete(key);
|
|
17
|
-
this.codes.delete(value.userCode);
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
start(name) {
|
|
22
|
-
this.cleanup();
|
|
23
|
-
if (typeof name !== 'string' || !name.trim() || name.length > 120 || /[\u0000-\u001f\u007f]/.test(name)) {
|
|
24
|
-
throw new Error('电脑名称格式无效。');
|
|
25
|
-
}
|
|
26
|
-
if (this.pending.size >= 1000) throw Object.assign(new Error('连接请求过多,请稍后再试。'), {status:429});
|
|
27
|
-
const deviceCode = crypto.randomBytes(32).toString('base64url');
|
|
28
|
-
let userCode;
|
|
29
|
-
do { userCode = crypto.randomBytes(5).toString('hex').toUpperCase(); } while (this.codes.has(userCode));
|
|
30
|
-
const key = digest(deviceCode);
|
|
31
|
-
const entry = {name:name.trim(), userCode, expiresAt:this.now() + this.lifetime, result:null};
|
|
32
|
-
this.pending.set(key, entry);
|
|
33
|
-
this.codes.set(userCode, key);
|
|
34
|
-
return {deviceCode, userCode, expiresIn:Math.floor(this.lifetime / 1000), interval:2};
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
preview(userCode) {
|
|
38
|
-
this.cleanup();
|
|
39
|
-
if (typeof userCode !== 'string') return null;
|
|
40
|
-
const entry = this.pending.get(this.codes.get(userCode));
|
|
41
|
-
return entry && !entry.result ? {name:entry.name, userCode:entry.userCode, expiresAt:entry.expiresAt} : null;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
approve(userCode, issue) {
|
|
45
|
-
if (!this.preview(userCode)) throw Object.assign(new Error('连接请求已失效,请在电脑重新运行 orbit login。'), {status:410});
|
|
46
|
-
const entry = this.pending.get(this.codes.get(userCode));
|
|
47
|
-
entry.result = issue(entry.name);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
poll(deviceCode) {
|
|
51
|
-
this.cleanup();
|
|
52
|
-
if (typeof deviceCode !== 'string' || !/^[a-zA-Z0-9_-]{43}$/.test(deviceCode)) return {status:410};
|
|
53
|
-
const key = digest(deviceCode);
|
|
54
|
-
const entry = this.pending.get(key);
|
|
55
|
-
if (!entry) return {status:410};
|
|
56
|
-
if (!entry.result) return {status:202};
|
|
57
|
-
return {status:200, result:structuredClone(entry.result)};
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
acknowledge(deviceCode) {
|
|
61
|
-
if (typeof deviceCode !== 'string' || !/^[a-zA-Z0-9_-]{43}$/.test(deviceCode)) return false;
|
|
62
|
-
const key = digest(deviceCode);
|
|
63
|
-
const entry = this.pending.get(key);
|
|
64
|
-
if (!entry?.result) return false;
|
|
65
|
-
this.pending.delete(key);
|
|
66
|
-
this.codes.delete(entry.userCode);
|
|
67
|
-
return true;
|
|
68
|
-
}
|
|
69
|
-
}
|