@walkhi/code-relax 0.1.0-beta.50 → 0.1.0-beta.51

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.
Files changed (89) hide show
  1. package/README.md +105 -105
  2. package/dist/bin/cleanup-accounts.mjs +63 -63
  3. package/dist/bin/codex-remote.mjs +403 -403
  4. package/dist/bin/publish-android-release.mjs +63 -63
  5. package/dist/bin/release-upload-server.mjs +190 -190
  6. package/dist/build-platform.json +1 -1
  7. package/dist/get-shared-service-url.ps1 +14 -14
  8. package/dist/shared/code-browser.cjs +159 -159
  9. package/dist/shared/file-changes.cjs +31 -31
  10. package/dist/shared/file-citations.cjs +117 -117
  11. package/dist/shared/file-mentions.cjs +92 -92
  12. package/dist/shared/message-delivery.cjs +115 -115
  13. package/dist/shared/p2p-file-channel.cjs +87 -87
  14. package/dist/shared/server-requests.cjs +311 -311
  15. package/dist/shared/timeline-sync.cjs +173 -173
  16. package/dist/skills/code-relax/SKILL.md +22 -22
  17. package/dist/src/bounded-process-log.mjs +63 -63
  18. package/dist/src/celsius-client.mjs +107 -107
  19. package/dist/src/dots-approval-timeline.mjs +99 -99
  20. package/dist/src/dots-avatar.mjs +42 -42
  21. package/dist/src/dots-chat-projection.mjs +50 -50
  22. package/dist/src/dots-chat-server.mjs +483 -483
  23. package/dist/src/dots-message-images.mjs +319 -319
  24. package/dist/src/dots-profile.mjs +77 -77
  25. package/dist/src/dots-room-activity.mjs +172 -172
  26. package/dist/src/dots-room-client.mjs +135 -135
  27. package/dist/src/dots-room-projection.mjs +94 -94
  28. package/dist/src/dots-schedules.mjs +66 -66
  29. package/dist/src/dots-summary-tasks.mjs +32 -32
  30. package/dist/src/dots-task-catalog.mjs +430 -430
  31. package/dist/src/file-transfer-range.mjs +22 -22
  32. package/dist/src/file-upload-session.mjs +130 -130
  33. package/dist/src/hosted-history-app-server.mjs +103 -103
  34. package/dist/src/hosted-history-cache.mjs +113 -113
  35. package/dist/src/lan-socket-server.mjs +266 -266
  36. package/dist/src/managed-app-server.mjs +373 -373
  37. package/dist/src/managed-endpoint.mjs +25 -25
  38. package/dist/src/message-upload-files.mjs +109 -109
  39. package/dist/src/onboarding.mjs +12 -12
  40. package/dist/src/platform/README.md +1 -1
  41. package/dist/src/platform/codex-protocol.mjs +46 -46
  42. package/dist/src/platform/desktop-project-store.mjs +208 -208
  43. package/dist/src/platform/linux/desktop-host.mjs +87 -87
  44. package/dist/src/platform/linux/process.mjs +53 -53
  45. package/dist/src/platform/linux/system.mjs +50 -50
  46. package/dist/src/platform/linux/user-environment.mjs +9 -9
  47. package/dist/src/platform/macos/codex-runtime.mjs +125 -125
  48. package/dist/src/platform/macos/desktop-host.mjs +86 -86
  49. package/dist/src/platform/macos/user-environment.mjs +9 -9
  50. package/dist/src/platform/native-package.mjs +58 -58
  51. package/dist/src/platform/windows/activate-desktop.ps1 +49 -49
  52. package/dist/src/platform/windows/codex-runtime.mjs +215 -215
  53. package/dist/src/platform/windows/credential-store.mjs +27 -27
  54. package/dist/src/platform/windows/credential-store.ps1 +49 -49
  55. package/dist/src/platform/windows/desktop-host.mjs +65 -65
  56. package/dist/src/platform/windows/desktop-info.ps1 +38 -38
  57. package/dist/src/platform/windows/desktop-project-registration.mjs +4 -4
  58. package/dist/src/platform/windows/desktop-shortcuts.ps1 +14 -14
  59. package/dist/src/platform/windows/dots-profile.mjs +49 -49
  60. package/dist/src/platform/windows/launch-worker.mjs +41 -41
  61. package/dist/src/platform/windows/project-locations.mjs +16 -16
  62. package/dist/src/platform/windows/service-host.mjs +35 -35
  63. package/dist/src/platform/windows/user-environment.mjs +52 -52
  64. package/dist/src/project-management.mjs +58 -58
  65. package/dist/src/runtime-discovery.mjs +47 -47
  66. package/dist/src/self-relay/account-cleanup.mjs +30 -30
  67. package/dist/src/self-relay/admin-page.mjs +80 -80
  68. package/dist/src/self-relay/admin-style.mjs +14 -14
  69. package/dist/src/self-relay/connector.mjs +439 -439
  70. package/dist/src/self-relay/demo.mjs +31 -31
  71. package/dist/src/self-relay/diagnostics.mjs +54 -54
  72. package/dist/src/self-relay/email-accounts.mjs +183 -183
  73. package/dist/src/self-relay/file-wire.mjs +13 -13
  74. package/dist/src/self-relay/p2p-probe.mjs +168 -168
  75. package/dist/src/self-relay/privacy-page.mjs +26 -26
  76. package/dist/src/self-relay/public-releases.mjs +67 -67
  77. package/dist/src/self-relay/rtc-file-bridge.mjs +41 -41
  78. package/dist/src/self-relay/usage-meter.mjs +141 -141
  79. package/dist/src/server.mjs +5 -4
  80. package/dist/src/service-doctor.mjs +148 -148
  81. package/dist/src/service-install.mjs +97 -97
  82. package/dist/src/service-lifecycle.mjs +139 -139
  83. package/dist/src/shared-app-server.mjs +666 -666
  84. package/dist/src/shared-catalog.mjs +132 -132
  85. package/dist/src/shared-thread-stream.mjs +165 -165
  86. package/dist/src/usage-reset.mjs +17 -17
  87. package/dist/src/workstation-downloads.mjs +120 -120
  88. package/package.json +4 -4
  89. package/tools/postinstall.mjs +186 -186
@@ -1,63 +1,63 @@
1
- #!/usr/bin/env node
2
- import fs from 'node:fs';
3
- import path from 'node:path';
4
- import { createHash } from 'node:crypto';
5
- import { androidReleaseStorage } from '../relay-admin/android-release-storage.mjs';
6
- import { readAndroidCatalog } from '../src/self-relay/android-releases.mjs';
7
- import { readPublicManifest, updatePublicAndroid } from '../src/self-relay/public-releases.mjs';
8
-
9
- // This administrative command runs only on the deployment host; no keys cross SSH.
10
- const [apkFile, metadataFile, mode] = process.argv.slice(2);
11
- if (mode && mode !== '--existing-object') throw new Error('Invalid publication mode');
12
- if (!apkFile || !metadataFile) throw new Error('Usage: publish-android-release.mjs APK METADATA');
13
- const storage = androidReleaseStorage('/etc/codex-relay/android-releases.json', '/etc/codex-relay/tencent-monitor.json');
14
- const metadata = JSON.parse(fs.readFileSync(metadataFile, 'utf8'));
15
- if (metadata.packageName !== 'com.walkhi.codexremote' || !/^[a-f0-9]{64}$/.test(metadata.certificateSha256)) throw new Error('Invalid APK identity');
16
- const { certificateSha256: expectedCertificate } = JSON.parse(fs.readFileSync('/etc/codex-relay/android-releases.json', 'utf8'));
17
- if (!/^[a-f0-9]{64}$/.test(expectedCertificate) || metadata.certificateSha256 !== expectedCertificate) throw new Error('APK signing certificate does not match release configuration');
18
- const bytes = fs.readFileSync(apkFile), sha256 = createHash('sha256').update(bytes).digest('hex');
19
- if (sha256 !== metadata.sha256 || bytes.length !== metadata.size) throw new Error('Uploaded APK integrity mismatch');
20
- const { versionCode, versionName, size, notes, certificateSha256 } = metadata;
21
- const release = { versionCode, versionName, size, sha256, notes, certificateSha256,
22
- objectKey: `code-relax/android/${versionCode}/Code-Relax-${sha256.slice(0, 16)}.apk` };
23
- const catalogFile = storage.catalogFile, temporary = catalogFile + '.new', lock = catalogFile + '.lock';
24
- const lockHandle = fs.openSync(lock, 'wx', 0o600);
25
- try {
26
- readPublicManifest();
27
- const catalog = fs.existsSync(catalogFile) ? readAndroidCatalog(catalogFile) : { releases: [] };
28
- const previous = catalog.releases.find(item => item.versionCode === versionCode);
29
- if (previous && previous.sha256 !== sha256) throw new Error('A different APK already uses this versionCode');
30
- if (catalog.latestVersionCode > versionCode) throw new Error('Refusing to publish an older version as latest');
31
- const next = { latestVersionCode: versionCode, releases: [...catalog.releases.filter(item => item.versionCode !== versionCode), release] };
32
- fs.writeFileSync(temporary, JSON.stringify(next, null, 2) + '\n', { mode: 0o640 });
33
- readAndroidCatalog(temporary);
34
- if (!previous && mode !== '--existing-object') {
35
- await new Promise((resolve, reject) => storage.cos.putObject({ ...storage.object(release), Body: bytes,
36
- ContentLength: bytes.length, ContentType: 'application/vnd.android.package-archive',
37
- ContentDisposition: `attachment; filename="Code-Relax-${versionName}.apk"`
38
- }, (error, value) => error ? reject(error) : resolve(value)));
39
- }
40
- // Validate the exact signed GET that website and App use, before switching latest.
41
- const ticket = await storage.signDownload(release);
42
- const response = await fetch(ticket.url, { signal: AbortSignal.timeout(120000) });
43
- if (!response.ok) throw new Error(`Signed download failed: HTTP ${response.status}`);
44
- const downloaded = Buffer.from(await response.arrayBuffer());
45
- if (downloaded.length !== size || createHash('sha256').update(downloaded).digest('hex') !== sha256) throw new Error('Stored APK integrity mismatch');
46
- const unsigned = new URL(ticket.url); unsigned.search = '';
47
- const denied = await fetch(unsigned, { method: 'HEAD', signal: AbortSignal.timeout(15000) });
48
- if (denied.status !== 403) throw new Error(`Private object check failed: HTTP ${denied.status}`);
49
- const owner = fs.statSync(path.dirname(catalogFile));
50
- fs.chownSync(temporary, owner.uid, owner.gid);
51
- fs.renameSync(temporary, catalogFile);
52
- updatePublicAndroid(release);
53
- console.log(JSON.stringify({ published: true, versionCode, versionName, size, sha256, unsignedStatus: denied.status,
54
- entry: 'https://www.coderelax.cn/code-relax/android' }));
55
- } catch (error) {
56
- // COS errors can carry signed request details. Keep CLI output credential-free.
57
- console.error('Android publication failed:', error?.code || (error?.name === 'Error' ? error.message : 'storage-request-failed'));
58
- process.exitCode = 1;
59
- } finally {
60
- fs.closeSync(lockHandle);
61
- fs.unlinkSync(lock);
62
- if (fs.existsSync(temporary)) fs.unlinkSync(temporary);
63
- }
1
+ #!/usr/bin/env node
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { createHash } from 'node:crypto';
5
+ import { androidReleaseStorage } from '../relay-admin/android-release-storage.mjs';
6
+ import { readAndroidCatalog } from '../src/self-relay/android-releases.mjs';
7
+ import { readPublicManifest, updatePublicAndroid } from '../src/self-relay/public-releases.mjs';
8
+
9
+ // This administrative command runs only on the deployment host; no keys cross SSH.
10
+ const [apkFile, metadataFile, mode] = process.argv.slice(2);
11
+ if (mode && mode !== '--existing-object') throw new Error('Invalid publication mode');
12
+ if (!apkFile || !metadataFile) throw new Error('Usage: publish-android-release.mjs APK METADATA');
13
+ const storage = androidReleaseStorage('/etc/codex-relay/android-releases.json', '/etc/codex-relay/tencent-monitor.json');
14
+ const metadata = JSON.parse(fs.readFileSync(metadataFile, 'utf8'));
15
+ if (metadata.packageName !== 'com.walkhi.codexremote' || !/^[a-f0-9]{64}$/.test(metadata.certificateSha256)) throw new Error('Invalid APK identity');
16
+ const { certificateSha256: expectedCertificate } = JSON.parse(fs.readFileSync('/etc/codex-relay/android-releases.json', 'utf8'));
17
+ if (!/^[a-f0-9]{64}$/.test(expectedCertificate) || metadata.certificateSha256 !== expectedCertificate) throw new Error('APK signing certificate does not match release configuration');
18
+ const bytes = fs.readFileSync(apkFile), sha256 = createHash('sha256').update(bytes).digest('hex');
19
+ if (sha256 !== metadata.sha256 || bytes.length !== metadata.size) throw new Error('Uploaded APK integrity mismatch');
20
+ const { versionCode, versionName, size, notes, certificateSha256 } = metadata;
21
+ const release = { versionCode, versionName, size, sha256, notes, certificateSha256,
22
+ objectKey: `code-relax/android/${versionCode}/Code-Relax-${sha256.slice(0, 16)}.apk` };
23
+ const catalogFile = storage.catalogFile, temporary = catalogFile + '.new', lock = catalogFile + '.lock';
24
+ const lockHandle = fs.openSync(lock, 'wx', 0o600);
25
+ try {
26
+ readPublicManifest();
27
+ const catalog = fs.existsSync(catalogFile) ? readAndroidCatalog(catalogFile) : { releases: [] };
28
+ const previous = catalog.releases.find(item => item.versionCode === versionCode);
29
+ if (previous && previous.sha256 !== sha256) throw new Error('A different APK already uses this versionCode');
30
+ if (catalog.latestVersionCode > versionCode) throw new Error('Refusing to publish an older version as latest');
31
+ const next = { latestVersionCode: versionCode, releases: [...catalog.releases.filter(item => item.versionCode !== versionCode), release] };
32
+ fs.writeFileSync(temporary, JSON.stringify(next, null, 2) + '\n', { mode: 0o640 });
33
+ readAndroidCatalog(temporary);
34
+ if (!previous && mode !== '--existing-object') {
35
+ await new Promise((resolve, reject) => storage.cos.putObject({ ...storage.object(release), Body: bytes,
36
+ ContentLength: bytes.length, ContentType: 'application/vnd.android.package-archive',
37
+ ContentDisposition: `attachment; filename="Code-Relax-${versionName}.apk"`
38
+ }, (error, value) => error ? reject(error) : resolve(value)));
39
+ }
40
+ // Validate the exact signed GET that website and App use, before switching latest.
41
+ const ticket = await storage.signDownload(release);
42
+ const response = await fetch(ticket.url, { signal: AbortSignal.timeout(120000) });
43
+ if (!response.ok) throw new Error(`Signed download failed: HTTP ${response.status}`);
44
+ const downloaded = Buffer.from(await response.arrayBuffer());
45
+ if (downloaded.length !== size || createHash('sha256').update(downloaded).digest('hex') !== sha256) throw new Error('Stored APK integrity mismatch');
46
+ const unsigned = new URL(ticket.url); unsigned.search = '';
47
+ const denied = await fetch(unsigned, { method: 'HEAD', signal: AbortSignal.timeout(15000) });
48
+ if (denied.status !== 403) throw new Error(`Private object check failed: HTTP ${denied.status}`);
49
+ const owner = fs.statSync(path.dirname(catalogFile));
50
+ fs.chownSync(temporary, owner.uid, owner.gid);
51
+ fs.renameSync(temporary, catalogFile);
52
+ updatePublicAndroid(release);
53
+ console.log(JSON.stringify({ published: true, versionCode, versionName, size, sha256, unsignedStatus: denied.status,
54
+ entry: 'https://www.coderelax.cn/code-relax/android' }));
55
+ } catch (error) {
56
+ // COS errors can carry signed request details. Keep CLI output credential-free.
57
+ console.error('Android publication failed:', error?.code || (error?.name === 'Error' ? error.message : 'storage-request-failed'));
58
+ process.exitCode = 1;
59
+ } finally {
60
+ fs.closeSync(lockHandle);
61
+ fs.unlinkSync(lock);
62
+ if (fs.existsSync(temporary)) fs.unlinkSync(temporary);
63
+ }
@@ -1,190 +1,190 @@
1
- #!/usr/bin/env node
2
- import fs from 'node:fs';
3
- import os from 'node:os';
4
- import path from 'node:path';
5
- import http from 'node:http';
6
- import { createHash, randomUUID } from 'node:crypto';
7
- import { pipeline } from 'node:stream/promises';
8
- import { Transform, Readable } from 'node:stream';
9
- import { execFile } from 'node:child_process';
10
- import { promisify } from 'node:util';
11
- import { fileURLToPath } from 'node:url';
12
- import { verifyReleaseIdentity, createUploadSigner } from '../relay-admin/release-oidc.mjs';
13
- import { androidReleaseStorage } from '../relay-admin/android-release-storage.mjs';
14
- import { readAndroidCatalog } from '../src/self-relay/android-releases.mjs';
15
- import { updatePublicWorkstations } from '../src/self-relay/public-releases.mjs';
16
-
17
- import { readHarmonyConfig, validateHarmonyMetadata, checkHarmonyVersion, verifyHarmony, activateHarmony } from '../relay-admin/harmony-release.mjs';
18
-
19
- const execute = promisify(execFile);
20
- const config = JSON.parse(fs.readFileSync('/etc/codex-relay/android-releases.json', 'utf8'));
21
- const credential = JSON.parse(fs.readFileSync('/etc/codex-relay/tencent-monitor.json', 'utf8'));
22
- const storage = androidReleaseStorage('/etc/codex-relay/android-releases.json', '/etc/codex-relay/tencent-monitor.json');
23
- const signUpload = createUploadSigner(config, { secretId: credential.secretId, secretKey: credential.secretKey });
24
- const stateDir = '/var/lib/codex-relay/release-publisher';
25
- fs.mkdirSync(stateDir, { recursive: true, mode: 0o700 });
26
- const cosCall = (method, args) => new Promise((resolve, reject) => storage.cos[method](args, (e, r) => e ? reject(e) : resolve(r)));
27
- const object = key => ({ Bucket: config.bucket, Region: config.region, Key: key });
28
- const identityKey = p => `${p.repository_id}/${p.run_id}/${p.run_attempt}/${p.sha}`;
29
- const filename = id => path.join(stateDir, id + '.json');
30
- function save(row) {
31
- const temp = filename(row.intentId) + '.' + randomUUID();
32
- fs.writeFileSync(temp, JSON.stringify(row), { mode: 0o600 }); fs.renameSync(temp, filename(row.intentId));
33
- }
34
- const read = id => fs.existsSync(filename(id)) ? JSON.parse(fs.readFileSync(filename(id))) : null;
35
- const publicState = row => ({ ok: true, intentId: row.intentId, status: row.status, result: row.result, error: row.error });
36
- async function readJson(req) {
37
- const chunks = []; let size = 0;
38
- for await (const chunk of req) { size += chunk.length; if (size > 16384) throw Error('Request too large'); chunks.push(chunk); }
39
- return JSON.parse(Buffer.concat(chunks).toString('utf8') || '{}');
40
- }
41
- function metadataFrom(data, claims, target) {
42
- const { packageName, versionCode, versionName, size, sha256, contentMd5, certificateSha256, notes,
43
- buildCommit, artifactId, runId, sourceCommit, verifyOnly } = data;
44
- if (!Number.isSafeInteger(versionCode) || versionCode < 1 || versionCode > 2100000000
45
- || !/^[a-f0-9]{64}$/.test(sha256)
46
- || !Number.isSafeInteger(size) || size < 1 || size > 200 * 1024 * 1024
47
- || typeof notes !== 'string' || notes.length > 2000 || !/^[A-Za-z0-9+/]{22}==$/.test(contentMd5)
48
- || !/^\d+$/.test(artifactId) || runId !== claims.run_id || sourceCommit !== claims.sha
49
- || typeof verifyOnly !== 'boolean' || !/^[a-f0-9]{40}$/.test(buildCommit)) throw Error('Invalid metadata');
50
- if (target === 'hap') validateHarmonyMetadata(data, readHarmonyConfig());
51
- else if (packageName !== 'com.walkhi.codexremote' || !/^\d+\.\d+\.\d+\.\d+$/.test(versionName)
52
- || !versionName.endsWith('.' + versionCode) || certificateSha256 !== config.certificateSha256) throw Error('Invalid Android identity');
53
- return { target, ...(target === 'hap' ? { files: data.files } : {}), packageName, versionCode, versionName, size, sha256, contentMd5, certificateSha256, notes,
54
- buildCommit, artifactId, runId, sourceCommit, verifyOnly };
55
- }
56
- let busy = false;
57
- const server = http.createServer(async (req, res) => {
58
- const reply = (code, data) => { if (!res.destroyed) { res.writeHead(code, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' }); res.end(JSON.stringify(data)); } };
59
- let claims;
60
- const npmStatus = req.url === '/code-relax/publish/npm/status';
61
- try { claims = await verifyReleaseIdentity((req.headers.authorization || '').replace(/^Bearer /, ''), npmStatus ? 'npm' : 'mobile'); }
62
- catch { reply(401, { ok: false, error: 'OIDC_REJECTED' }); return; }
63
- if (npmStatus) {
64
- try {
65
- if (req.method !== 'POST') { reply(405, { ok: false, error: 'METHOD_NOT_ALLOWED' }); return; }
66
- const { version } = await readJson(req);
67
- const response = await fetch('https://registry.npmjs.org/@walkhi%2fcode-relax', { signal: AbortSignal.timeout(15000) });
68
- if (!response.ok) throw Error('Registry unavailable');
69
- const metadata = await response.json();
70
- if (metadata['dist-tags']?.latest !== version || metadata['dist-tags']?.beta !== version
71
- || !metadata.versions?.[version]) { reply(409, { ok: false, error: 'NPM_VERSION_MISMATCH' }); return; }
72
- updatePublicWorkstations(version);
73
- reply(200, { ok: true, version });
74
- } catch { reply(500, { ok: false, error: 'NPM_STATUS_SYNC_FAILED' }); }
75
- return;
76
- }
77
- const target = /^\/code-relax\/publish\/(android|hap)\/intents(?:\/|$)/.exec(req.url || '')?.[1];
78
- if (!target) { reply(404, { ok: false, error: 'NOT_FOUND' }); return; }
79
- const prefix = `/code-relax/publish/${target}/intents`;
80
- try {
81
- if (req.method === 'POST' && req.url === prefix) {
82
- let metadata;
83
- try { metadata = metadataFrom(await readJson(req), claims, target); } catch { reply(400, { ok: false, error: 'INVALID_RELEASE' }); return; }
84
- if (!metadata.verifyOnly && target === 'hap') {
85
- try { checkHarmonyVersion(metadata); } catch { reply(409, { ok: false, error: 'VERSION_CONFLICT' }); return; }
86
- }
87
- if (!metadata.verifyOnly && target === 'android') {
88
- const catalog = readAndroidCatalog(config.catalogFile);
89
- const previous = catalog.releases.find(x => x.versionCode === metadata.versionCode);
90
- if (catalog.latestVersionCode > metadata.versionCode || (previous && previous.sha256 !== metadata.sha256)) {
91
- reply(409, { ok: false, error: 'VERSION_CONFLICT' }); return;
92
- }
93
- }
94
- const identity = identityKey(claims);
95
- const intentId = createHash('sha256').update(JSON.stringify({ identity, metadata })).digest('hex');
96
- let row = read(intentId);
97
- if (row && row.status !== 'prepared') { reply(200, publicState(row)); return; }
98
- const key = metadata.verifyOnly ? `code-relax/android/ci-verification/${claims.run_id}/${intentId}.apk`
99
- : `code-relax/android/${metadata.versionCode}/Code-Relax-${metadata.sha256.slice(0, 16)}.apk`;
100
- const keys = target === 'hap' ? metadata.files.map(f => metadata.verifyOnly
101
- ? `code-relax/harmony/ci-verification/${claims.run_id}/${intentId}/${f.name}`
102
- : `code-relax/harmony/${metadata.versionCode}/${f.name}`) : [key];
103
- row ??= { intentId, identity, metadata, key, keys, status: 'prepared' };
104
- save(row);
105
- const grant = target === 'hap' ? { uploads: await Promise.all(metadata.files.map(async (f, i) => ({ name: f.name,
106
- ...await signUpload(keys[i], f, i === 1 ? 'application/json' : i === 2 ? 'image/png' : 'application/octet-stream') }))) }
107
- : await signUpload(key, metadata);
108
- reply(200, { ...publicState(row), ...grant }); return;
109
- }
110
- const match = new RegExp(`^${prefix}/([a-f0-9]{64})(/complete)?$`).exec(req.url || '');
111
- if (!match || !((!match[2] && req.method === 'GET') || (match[2] && req.method === 'POST'))) {
112
- reply(404, { ok: false, error: 'NOT_FOUND' }); return;
113
- }
114
- const row = read(match[1]);
115
- if (!row || row.identity !== identityKey(claims) || (row.metadata.target || 'android') !== target) { reply(404, { ok: false, error: 'INTENT_NOT_FOUND' }); return; }
116
- if (match[2] && row.status === 'prepared') {
117
- if (busy) { reply(409, { ok: false, error: 'PUBLICATION_BUSY' }); return; }
118
- busy = true; row.status = 'validating'; save(row);
119
- void complete(row).finally(() => { busy = false; });
120
- }
121
- reply(row.status === 'validating' ? 202 : 200, publicState(row));
122
- } catch (error) {
123
- console.error(JSON.stringify({ event: 'release-api-failed', code: /^[A-Za-z.]+$/.test(error.code || '') ? error.code : 'io-or-provider' }));
124
- reply(503, { ok: false, error: 'RELEASE_UNAVAILABLE' });
125
- }
126
- });
127
- async function complete(row) {
128
- const { metadata, key } = row;
129
- const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'code-relax-release-'));
130
- const apk = path.join(dir, 'release.apk'), json = path.join(dir, 'release.json');
131
- const hap = metadata.target === 'hap';
132
- const files = hap ? metadata.files : [{ ...metadata, name: 'release.apk' }];
133
- let stage = 'download';
134
- try {
135
- for (const [i, file] of files.entries()) {
136
- const ticket = await cosCall('getObjectUrl', { ...object(row.keys?.[i] || key), Sign: true, Method: 'GET', Protocol: 'https:', Expires: 300 });
137
- const response = await fetch(ticket.Url, { signal: AbortSignal.timeout(120000) });
138
- if (!response.ok || Number(response.headers.get('content-length')) !== file.size) throw Error();
139
- let size = 0; const hash = createHash('sha256'), md5 = createHash('md5');
140
- await pipeline(Readable.fromWeb(response.body), new Transform({ transform(chunk, encoding, next) {
141
- size += chunk.length; if (size > file.size) return next(Error('Oversize'));
142
- hash.update(chunk); md5.update(chunk); next(null, chunk);
143
- } }), fs.createWriteStream(path.join(dir, file.name), { flags: 'wx', mode: 0o600 }));
144
- if (size !== file.size || hash.digest('hex') !== file.sha256 || md5.digest('base64') !== file.contentMd5) throw Error();
145
- const unsigned = new URL(ticket.Url); unsigned.search = '';
146
- if (!hap && (await fetch(unsigned, { method: 'HEAD', signal: AbortSignal.timeout(15000) })).status !== 403) throw Error();
147
- }
148
- if (hap) {
149
- stage = 'harmony-signature';
150
- await verifyHarmony(dir, metadata, readHarmonyConfig());
151
- row.result = { sha256: metadata.sha256, versionCode: metadata.versionCode, size: metadata.size };
152
- if (metadata.verifyOnly) row.status = 'verified';
153
- else { stage = 'harmony-publish'; row.result = await activateHarmony(metadata, storage, row.keys); row.status = 'published'; }
154
- save(row); return;
155
- }
156
- stage = 'signature';
157
- const { stdout: signature } = await execute('/usr/bin/apksigner', ['verify', '--print-certs', apk], {
158
- timeout: 30000, env: { ...process.env, JAVA_TOOL_OPTIONS: '-Xmx256m' } });
159
- const certificates = [...signature.matchAll(/^Signer #\d+ certificate SHA-256 digest: ([a-f0-9]{64})$/gm)];
160
- if (certificates.length !== 1 || certificates[0][1] !== metadata.certificateSha256) throw Error();
161
- stage = 'identity';
162
- const { stdout: badging } = await execute('/usr/bin/aapt', ['dump', 'badging', apk], { timeout: 30000 });
163
- const identity = /^package: name='([^']+)' versionCode='(\d+)' versionName='([^']+)'/m.exec(badging);
164
- if (!identity || identity[1] !== metadata.packageName || Number(identity[2]) !== metadata.versionCode
165
- || identity[3] !== metadata.versionName || /^application-debuggable/m.test(badging)) throw Error();
166
- row.result = { sha256: metadata.sha256, versionCode: metadata.versionCode, size: metadata.size };
167
- if (metadata.verifyOnly) row.status = 'verified';
168
- else {
169
- stage = 'publish';
170
- fs.writeFileSync(json, JSON.stringify(metadata), { mode: 0o600 });
171
- const { stdout } = await execute(process.execPath, [fileURLToPath(new URL('./publish-android-release.mjs', import.meta.url)), apk, json, '--existing-object'], { timeout: 240000 });
172
- row.result = JSON.parse(stdout.trim()); row.status = 'published';
173
- }
174
- save(row);
175
- } catch (error) {
176
- row.status = 'failed'; row.error = `RELEASE_${stage.toUpperCase()}_FAILED`; save(row);
177
- console.error(JSON.stringify({ event: 'release-validation-failed', intentId: row.intentId, stage, code: /^[A-Za-z0-9_.-]+$/.test(error.code || '') ? error.code : 'validation' }));
178
- } finally {
179
- for (const file of [...files.map(f => path.join(dir, f.name)), json]) fs.rmSync(file, { force: true }); fs.rmdirSync(dir);
180
- // Verification objects remain private evidence; the deployment identity has no DeleteObject grant.
181
- }
182
- }
183
- // A restart never blindly launches an interrupted publisher a second time.
184
- for (const name of fs.readdirSync(stateDir).filter(x => /^[a-f0-9]{64}\.json$/.test(x))) {
185
- const row = read(name.slice(0, -5));
186
- if (row.status === 'validating') { row.status = 'failed'; row.error = 'INTERRUPTED_CHECK_LATEST'; save(row); }
187
- }
188
- server.requestTimeout = 15000;
189
- server.listen(45852, '127.0.0.1');
190
- process.on('SIGTERM', () => { server.close(); const timer = setInterval(() => { if (!busy) { clearInterval(timer); process.exit(0); } }, 250); });
1
+ #!/usr/bin/env node
2
+ import fs from 'node:fs';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import http from 'node:http';
6
+ import { createHash, randomUUID } from 'node:crypto';
7
+ import { pipeline } from 'node:stream/promises';
8
+ import { Transform, Readable } from 'node:stream';
9
+ import { execFile } from 'node:child_process';
10
+ import { promisify } from 'node:util';
11
+ import { fileURLToPath } from 'node:url';
12
+ import { verifyReleaseIdentity, createUploadSigner } from '../relay-admin/release-oidc.mjs';
13
+ import { androidReleaseStorage } from '../relay-admin/android-release-storage.mjs';
14
+ import { readAndroidCatalog } from '../src/self-relay/android-releases.mjs';
15
+ import { updatePublicWorkstations } from '../src/self-relay/public-releases.mjs';
16
+
17
+ import { readHarmonyConfig, validateHarmonyMetadata, checkHarmonyVersion, verifyHarmony, activateHarmony } from '../relay-admin/harmony-release.mjs';
18
+
19
+ const execute = promisify(execFile);
20
+ const config = JSON.parse(fs.readFileSync('/etc/codex-relay/android-releases.json', 'utf8'));
21
+ const credential = JSON.parse(fs.readFileSync('/etc/codex-relay/tencent-monitor.json', 'utf8'));
22
+ const storage = androidReleaseStorage('/etc/codex-relay/android-releases.json', '/etc/codex-relay/tencent-monitor.json');
23
+ const signUpload = createUploadSigner(config, { secretId: credential.secretId, secretKey: credential.secretKey });
24
+ const stateDir = '/var/lib/codex-relay/release-publisher';
25
+ fs.mkdirSync(stateDir, { recursive: true, mode: 0o700 });
26
+ const cosCall = (method, args) => new Promise((resolve, reject) => storage.cos[method](args, (e, r) => e ? reject(e) : resolve(r)));
27
+ const object = key => ({ Bucket: config.bucket, Region: config.region, Key: key });
28
+ const identityKey = p => `${p.repository_id}/${p.run_id}/${p.run_attempt}/${p.sha}`;
29
+ const filename = id => path.join(stateDir, id + '.json');
30
+ function save(row) {
31
+ const temp = filename(row.intentId) + '.' + randomUUID();
32
+ fs.writeFileSync(temp, JSON.stringify(row), { mode: 0o600 }); fs.renameSync(temp, filename(row.intentId));
33
+ }
34
+ const read = id => fs.existsSync(filename(id)) ? JSON.parse(fs.readFileSync(filename(id))) : null;
35
+ const publicState = row => ({ ok: true, intentId: row.intentId, status: row.status, result: row.result, error: row.error });
36
+ async function readJson(req) {
37
+ const chunks = []; let size = 0;
38
+ for await (const chunk of req) { size += chunk.length; if (size > 16384) throw Error('Request too large'); chunks.push(chunk); }
39
+ return JSON.parse(Buffer.concat(chunks).toString('utf8') || '{}');
40
+ }
41
+ function metadataFrom(data, claims, target) {
42
+ const { packageName, versionCode, versionName, size, sha256, contentMd5, certificateSha256, notes,
43
+ buildCommit, artifactId, runId, sourceCommit, verifyOnly } = data;
44
+ if (!Number.isSafeInteger(versionCode) || versionCode < 1 || versionCode > 2100000000
45
+ || !/^[a-f0-9]{64}$/.test(sha256)
46
+ || !Number.isSafeInteger(size) || size < 1 || size > 200 * 1024 * 1024
47
+ || typeof notes !== 'string' || notes.length > 2000 || !/^[A-Za-z0-9+/]{22}==$/.test(contentMd5)
48
+ || !/^\d+$/.test(artifactId) || runId !== claims.run_id || sourceCommit !== claims.sha
49
+ || typeof verifyOnly !== 'boolean' || !/^[a-f0-9]{40}$/.test(buildCommit)) throw Error('Invalid metadata');
50
+ if (target === 'hap') validateHarmonyMetadata(data, readHarmonyConfig());
51
+ else if (packageName !== 'com.walkhi.codexremote' || !/^\d+\.\d+\.\d+\.\d+$/.test(versionName)
52
+ || !versionName.endsWith('.' + versionCode) || certificateSha256 !== config.certificateSha256) throw Error('Invalid Android identity');
53
+ return { target, ...(target === 'hap' ? { files: data.files } : {}), packageName, versionCode, versionName, size, sha256, contentMd5, certificateSha256, notes,
54
+ buildCommit, artifactId, runId, sourceCommit, verifyOnly };
55
+ }
56
+ let busy = false;
57
+ const server = http.createServer(async (req, res) => {
58
+ const reply = (code, data) => { if (!res.destroyed) { res.writeHead(code, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' }); res.end(JSON.stringify(data)); } };
59
+ let claims;
60
+ const npmStatus = req.url === '/code-relax/publish/npm/status';
61
+ try { claims = await verifyReleaseIdentity((req.headers.authorization || '').replace(/^Bearer /, ''), npmStatus ? 'npm' : 'mobile'); }
62
+ catch { reply(401, { ok: false, error: 'OIDC_REJECTED' }); return; }
63
+ if (npmStatus) {
64
+ try {
65
+ if (req.method !== 'POST') { reply(405, { ok: false, error: 'METHOD_NOT_ALLOWED' }); return; }
66
+ const { version } = await readJson(req);
67
+ const response = await fetch('https://registry.npmjs.org/@walkhi%2fcode-relax', { signal: AbortSignal.timeout(15000) });
68
+ if (!response.ok) throw Error('Registry unavailable');
69
+ const metadata = await response.json();
70
+ if (metadata['dist-tags']?.latest !== version || metadata['dist-tags']?.beta !== version
71
+ || !metadata.versions?.[version]) { reply(409, { ok: false, error: 'NPM_VERSION_MISMATCH' }); return; }
72
+ updatePublicWorkstations(version);
73
+ reply(200, { ok: true, version });
74
+ } catch { reply(500, { ok: false, error: 'NPM_STATUS_SYNC_FAILED' }); }
75
+ return;
76
+ }
77
+ const target = /^\/code-relax\/publish\/(android|hap)\/intents(?:\/|$)/.exec(req.url || '')?.[1];
78
+ if (!target) { reply(404, { ok: false, error: 'NOT_FOUND' }); return; }
79
+ const prefix = `/code-relax/publish/${target}/intents`;
80
+ try {
81
+ if (req.method === 'POST' && req.url === prefix) {
82
+ let metadata;
83
+ try { metadata = metadataFrom(await readJson(req), claims, target); } catch { reply(400, { ok: false, error: 'INVALID_RELEASE' }); return; }
84
+ if (!metadata.verifyOnly && target === 'hap') {
85
+ try { checkHarmonyVersion(metadata); } catch { reply(409, { ok: false, error: 'VERSION_CONFLICT' }); return; }
86
+ }
87
+ if (!metadata.verifyOnly && target === 'android') {
88
+ const catalog = readAndroidCatalog(config.catalogFile);
89
+ const previous = catalog.releases.find(x => x.versionCode === metadata.versionCode);
90
+ if (catalog.latestVersionCode > metadata.versionCode || (previous && previous.sha256 !== metadata.sha256)) {
91
+ reply(409, { ok: false, error: 'VERSION_CONFLICT' }); return;
92
+ }
93
+ }
94
+ const identity = identityKey(claims);
95
+ const intentId = createHash('sha256').update(JSON.stringify({ identity, metadata })).digest('hex');
96
+ let row = read(intentId);
97
+ if (row && row.status !== 'prepared') { reply(200, publicState(row)); return; }
98
+ const key = metadata.verifyOnly ? `code-relax/android/ci-verification/${claims.run_id}/${intentId}.apk`
99
+ : `code-relax/android/${metadata.versionCode}/Code-Relax-${metadata.sha256.slice(0, 16)}.apk`;
100
+ const keys = target === 'hap' ? metadata.files.map(f => metadata.verifyOnly
101
+ ? `code-relax/harmony/ci-verification/${claims.run_id}/${intentId}/${f.name}`
102
+ : `code-relax/harmony/${metadata.versionCode}/${f.name}`) : [key];
103
+ row ??= { intentId, identity, metadata, key, keys, status: 'prepared' };
104
+ save(row);
105
+ const grant = target === 'hap' ? { uploads: await Promise.all(metadata.files.map(async (f, i) => ({ name: f.name,
106
+ ...await signUpload(keys[i], f, i === 1 ? 'application/json' : i === 2 ? 'image/png' : 'application/octet-stream') }))) }
107
+ : await signUpload(key, metadata);
108
+ reply(200, { ...publicState(row), ...grant }); return;
109
+ }
110
+ const match = new RegExp(`^${prefix}/([a-f0-9]{64})(/complete)?$`).exec(req.url || '');
111
+ if (!match || !((!match[2] && req.method === 'GET') || (match[2] && req.method === 'POST'))) {
112
+ reply(404, { ok: false, error: 'NOT_FOUND' }); return;
113
+ }
114
+ const row = read(match[1]);
115
+ if (!row || row.identity !== identityKey(claims) || (row.metadata.target || 'android') !== target) { reply(404, { ok: false, error: 'INTENT_NOT_FOUND' }); return; }
116
+ if (match[2] && row.status === 'prepared') {
117
+ if (busy) { reply(409, { ok: false, error: 'PUBLICATION_BUSY' }); return; }
118
+ busy = true; row.status = 'validating'; save(row);
119
+ void complete(row).finally(() => { busy = false; });
120
+ }
121
+ reply(row.status === 'validating' ? 202 : 200, publicState(row));
122
+ } catch (error) {
123
+ console.error(JSON.stringify({ event: 'release-api-failed', code: /^[A-Za-z.]+$/.test(error.code || '') ? error.code : 'io-or-provider' }));
124
+ reply(503, { ok: false, error: 'RELEASE_UNAVAILABLE' });
125
+ }
126
+ });
127
+ async function complete(row) {
128
+ const { metadata, key } = row;
129
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'code-relax-release-'));
130
+ const apk = path.join(dir, 'release.apk'), json = path.join(dir, 'release.json');
131
+ const hap = metadata.target === 'hap';
132
+ const files = hap ? metadata.files : [{ ...metadata, name: 'release.apk' }];
133
+ let stage = 'download';
134
+ try {
135
+ for (const [i, file] of files.entries()) {
136
+ const ticket = await cosCall('getObjectUrl', { ...object(row.keys?.[i] || key), Sign: true, Method: 'GET', Protocol: 'https:', Expires: 300 });
137
+ const response = await fetch(ticket.Url, { signal: AbortSignal.timeout(120000) });
138
+ if (!response.ok || Number(response.headers.get('content-length')) !== file.size) throw Error();
139
+ let size = 0; const hash = createHash('sha256'), md5 = createHash('md5');
140
+ await pipeline(Readable.fromWeb(response.body), new Transform({ transform(chunk, encoding, next) {
141
+ size += chunk.length; if (size > file.size) return next(Error('Oversize'));
142
+ hash.update(chunk); md5.update(chunk); next(null, chunk);
143
+ } }), fs.createWriteStream(path.join(dir, file.name), { flags: 'wx', mode: 0o600 }));
144
+ if (size !== file.size || hash.digest('hex') !== file.sha256 || md5.digest('base64') !== file.contentMd5) throw Error();
145
+ const unsigned = new URL(ticket.Url); unsigned.search = '';
146
+ if (!hap && (await fetch(unsigned, { method: 'HEAD', signal: AbortSignal.timeout(15000) })).status !== 403) throw Error();
147
+ }
148
+ if (hap) {
149
+ stage = 'harmony-signature';
150
+ await verifyHarmony(dir, metadata, readHarmonyConfig());
151
+ row.result = { sha256: metadata.sha256, versionCode: metadata.versionCode, size: metadata.size };
152
+ if (metadata.verifyOnly) row.status = 'verified';
153
+ else { stage = 'harmony-publish'; row.result = await activateHarmony(metadata, storage, row.keys); row.status = 'published'; }
154
+ save(row); return;
155
+ }
156
+ stage = 'signature';
157
+ const { stdout: signature } = await execute('/usr/bin/apksigner', ['verify', '--print-certs', apk], {
158
+ timeout: 30000, env: { ...process.env, JAVA_TOOL_OPTIONS: '-Xmx256m' } });
159
+ const certificates = [...signature.matchAll(/^Signer #\d+ certificate SHA-256 digest: ([a-f0-9]{64})$/gm)];
160
+ if (certificates.length !== 1 || certificates[0][1] !== metadata.certificateSha256) throw Error();
161
+ stage = 'identity';
162
+ const { stdout: badging } = await execute('/usr/bin/aapt', ['dump', 'badging', apk], { timeout: 30000 });
163
+ const identity = /^package: name='([^']+)' versionCode='(\d+)' versionName='([^']+)'/m.exec(badging);
164
+ if (!identity || identity[1] !== metadata.packageName || Number(identity[2]) !== metadata.versionCode
165
+ || identity[3] !== metadata.versionName || /^application-debuggable/m.test(badging)) throw Error();
166
+ row.result = { sha256: metadata.sha256, versionCode: metadata.versionCode, size: metadata.size };
167
+ if (metadata.verifyOnly) row.status = 'verified';
168
+ else {
169
+ stage = 'publish';
170
+ fs.writeFileSync(json, JSON.stringify(metadata), { mode: 0o600 });
171
+ const { stdout } = await execute(process.execPath, [fileURLToPath(new URL('./publish-android-release.mjs', import.meta.url)), apk, json, '--existing-object'], { timeout: 240000 });
172
+ row.result = JSON.parse(stdout.trim()); row.status = 'published';
173
+ }
174
+ save(row);
175
+ } catch (error) {
176
+ row.status = 'failed'; row.error = `RELEASE_${stage.toUpperCase()}_FAILED`; save(row);
177
+ console.error(JSON.stringify({ event: 'release-validation-failed', intentId: row.intentId, stage, code: /^[A-Za-z0-9_.-]+$/.test(error.code || '') ? error.code : 'validation' }));
178
+ } finally {
179
+ for (const file of [...files.map(f => path.join(dir, f.name)), json]) fs.rmSync(file, { force: true }); fs.rmdirSync(dir);
180
+ // Verification objects remain private evidence; the deployment identity has no DeleteObject grant.
181
+ }
182
+ }
183
+ // A restart never blindly launches an interrupted publisher a second time.
184
+ for (const name of fs.readdirSync(stateDir).filter(x => /^[a-f0-9]{64}\.json$/.test(x))) {
185
+ const row = read(name.slice(0, -5));
186
+ if (row.status === 'validating') { row.status = 'failed'; row.error = 'INTERRUPTED_CHECK_LATEST'; save(row); }
187
+ }
188
+ server.requestTimeout = 15000;
189
+ server.listen(45852, '127.0.0.1');
190
+ process.on('SIGTERM', () => { server.close(); const timer = setInterval(() => { if (!busy) { clearInterval(timer); process.exit(0); } }, 250); });
@@ -1 +1 @@
1
- {"layout":"npm-platform-packages","version":"0.1.0-beta.50"}
1
+ {"layout":"npm-platform-packages","version":"0.1.0-beta.51"}
@@ -1,14 +1,14 @@
1
- param([Parameter(Mandatory=$true)][string]$AppServerUrl)
2
- $ErrorActionPreference = 'Stop'
3
- [Console]::OutputEncoding = New-Object System.Text.UTF8Encoding($false)
4
- if ($AppServerUrl -cnotmatch '^ws://127\.0\.0\.1:([1-9][0-9]{0,4})\z' -or [int]$Matches[1] -gt 65535) { throw 'Invalid managed app-server URL.' }
5
- $port = [int]$Matches[1]
6
- $package = Get-AppxPackage OpenAI.Codex | Select-Object -First 1
7
- if (-not $package) { exit }
8
- $desktop = Join-Path $package.InstallLocation 'app/ChatGPT.exe'
9
- # Match an installed Desktop process connected to the selected loopback port.
10
- $connections = @(Get-NetTCPConnection -RemotePort $port -State Established -ErrorAction SilentlyContinue | Where-Object {
11
- $_.RemoteAddress -eq '127.0.0.1' -and
12
- (Get-Process -Id $_.OwningProcess -ErrorAction SilentlyContinue).Path -eq $desktop
13
- })
14
- if ($connections.Count) { $AppServerUrl }
1
+ param([Parameter(Mandatory=$true)][string]$AppServerUrl)
2
+ $ErrorActionPreference = 'Stop'
3
+ [Console]::OutputEncoding = New-Object System.Text.UTF8Encoding($false)
4
+ if ($AppServerUrl -cnotmatch '^ws://127\.0\.0\.1:([1-9][0-9]{0,4})\z' -or [int]$Matches[1] -gt 65535) { throw 'Invalid managed app-server URL.' }
5
+ $port = [int]$Matches[1]
6
+ $package = Get-AppxPackage OpenAI.Codex | Select-Object -First 1
7
+ if (-not $package) { exit }
8
+ $desktop = Join-Path $package.InstallLocation 'app/ChatGPT.exe'
9
+ # Match an installed Desktop process connected to the selected loopback port.
10
+ $connections = @(Get-NetTCPConnection -RemotePort $port -State Established -ErrorAction SilentlyContinue | Where-Object {
11
+ $_.RemoteAddress -eq '127.0.0.1' -and
12
+ (Get-Process -Id $_.OwningProcess -ErrorAction SilentlyContinue).Path -eq $desktop
13
+ })
14
+ if ($connections.Count) { $AppServerUrl }