oft-agt 0.5.15 → 0.5.17
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/cli.mjs +12 -7
- package/compile-plan.mjs +2 -2
- package/compiler.mjs +2 -0
- package/npm-shrinkwrap.json +17 -17
- package/package.json +1 -1
- package/runtime/api-request.mjs +9 -2
- package/runtime/version.mjs +1 -0
- package/usage.mjs +1 -1
- package/version.mjs +1 -1
- package/README.md +0 -36
package/cli.mjs
CHANGED
|
@@ -30,6 +30,7 @@ let child;
|
|
|
30
30
|
let active;
|
|
31
31
|
let usage;
|
|
32
32
|
let usageReady = true;
|
|
33
|
+
async function runtimeVersion(){try{const state=JSON.parse(await readFile(path.join(home,'active.json'),'utf8'));if(!/^[a-f0-9-]{36}$/.test(state.id))return null;const meta=JSON.parse(await readFile(path.join(home,'releases',state.id,'runtime-version.json'),'utf8'));return /^\d+\.\d+\.\d+$/.test(meta.version)?meta.version:null;}catch{return null;}}
|
|
33
34
|
function watchUsage(process) { process.on('message', message => { try { usage?.record(message); } catch { console.error('Mesure statistique ignorée.'); } }); return process; }
|
|
34
35
|
async function exists(p) { try { await access(p); return true; } catch { return false; } }
|
|
35
36
|
async function save(p, data) { await writeFile(p + '.tmp', typeof data === 'string' ? data : JSON.stringify(data), { mode: 0o600 }); await rename(p + '.tmp', p); }
|
|
@@ -50,6 +51,7 @@ async function credentials(config) {
|
|
|
50
51
|
}
|
|
51
52
|
async function stopBot() {
|
|
52
53
|
const current = child;
|
|
54
|
+
child = null;
|
|
53
55
|
if (!current || current.exitCode !== null || current.signalCode) return;
|
|
54
56
|
await new Promise(resolve => { const timer = setTimeout(() => current.kill('SIGKILL'), 5000); current.once('exit', () => { clearTimeout(timer); resolve(); }); current.kill('SIGTERM'); });
|
|
55
57
|
}
|
|
@@ -88,6 +90,7 @@ async function deploy(config, message, notify) {
|
|
|
88
90
|
await run(process.execPath, ['bot.mjs', '--deploy'], { cwd: target, env, stdio: 'ignore' });
|
|
89
91
|
const previous = child;
|
|
90
92
|
const previousId = active;
|
|
93
|
+
child = null;
|
|
91
94
|
if (previous && previous.exitCode === null && !previous.signalCode) {
|
|
92
95
|
await new Promise(resolve => { const timeout = setTimeout(() => { previous.kill('SIGKILL'); }, 5000); previous.once('exit', () => { clearTimeout(timeout); resolve(); }); previous.kill('SIGTERM'); });
|
|
93
96
|
}
|
|
@@ -108,7 +111,8 @@ async function deploy(config, message, notify) {
|
|
|
108
111
|
}
|
|
109
112
|
throw error;
|
|
110
113
|
}
|
|
111
|
-
child
|
|
114
|
+
const deployedChild=child;
|
|
115
|
+
deployedChild.once('exit', () => { if (child === deployedChild && active === message.id) notify('runtime-failed'); });
|
|
112
116
|
active = message.id;
|
|
113
117
|
await save(path.join(home, 'active.json'), { id: message.id });
|
|
114
118
|
notify('running');
|
|
@@ -161,7 +165,7 @@ async function start(config) {
|
|
|
161
165
|
try { const response = await fetch('https://discord.com/api/v10/users/@me', { headers: { Authorization: 'Bot ' + secret.token }, signal: AbortSignal.timeout(15000) }); if (response.ok) { const user = await response.json(); profile = { id: user.id, username: user.username, avatar: user.avatar }; } } finally { secret.token = ''; }
|
|
162
166
|
} catch {}
|
|
163
167
|
}
|
|
164
|
-
send({ type: 'hello', agentVersion: AGENT_VERSION, updatesVersion: 1, officialUpdatesVersion: 1, protocol: 2, errorBranch: true, diagnosticsVersion: 2, optionsVersion: 1, conditionsVersion: 1, eventsVersion: 1, apiVersion: 1, presenceVersion:1, complementaryBlocksVersion: 1, discordActionsVersion: 1, messageButtonsVersion: 2, variablesVersion: Number(process.versions.node.split('.')[0]) > 22 || (Number(process.versions.node.split('.')[0]) === 22 && Number(process.versions.node.split('.')[1]) >= 5) ? 1 : 0, metricsVersion: usageReady ? 1 : 0, profile, running: Boolean(child && child.exitCode === null && !child.signalCode), hasToken: await exists(secretPath), deploymentId: active });
|
|
168
|
+
send({ type: 'hello', agentVersion: AGENT_VERSION, runtimeVersion:await runtimeVersion(), updatesVersion: 1, officialUpdatesVersion: 1, protocol: 2, errorBranch: true, diagnosticsVersion: 2, optionsVersion: 1, conditionsVersion: 1, eventsVersion: 1, apiVersion: 1, presenceVersion:1, complementaryBlocksVersion: 1, discordActionsVersion: 1, messageButtonsVersion: 2, variablesVersion: Number(process.versions.node.split('.')[0]) > 22 || (Number(process.versions.node.split('.')[0]) === 22 && Number(process.versions.node.split('.')[1]) >= 5) ? 1 : 0, metricsVersion: usageReady ? 1 : 0, profile, running: Boolean(child && child.exitCode === null && !child.signalCode), hasToken: await exists(secretPath), deploymentId: active });
|
|
165
169
|
if (process.connected) process.send?.({ type: 'agent-ready' }, () => {});
|
|
166
170
|
await usage.flush(send).catch(() => console.error('Statistiques locales indisponibles.'));
|
|
167
171
|
usageTimer = setInterval(() => usage.flush(send).catch(() => console.error('Statistiques locales indisponibles.')), 60000);
|
|
@@ -202,7 +206,7 @@ async function start(config) {
|
|
|
202
206
|
if (message.type === 'deploy') {
|
|
203
207
|
console.log(`[${new Date().toISOString()}] Nouveau déploiement reçu.`);
|
|
204
208
|
if (await exists(path.join(home, 'paused.json'))) await unlink(path.join(home, 'paused.json'));
|
|
205
|
-
try { await deploy(config, message, state => { send({ type: 'deployment-status', id: message.id, state }); console.log(({ received: 'Déploiement validé.', installing: 'Préparation du bot.', running: 'Déploiement terminé. Bot démarré.', failed: 'Échec du déploiement.' })[state] || 'État du déploiement modifié.'); }); }
|
|
209
|
+
try { await deploy(config, message, async state => { if(state==='runtime-failed'){send({type:'runtime-status',state:'failed'});return;} send({ type: 'deployment-status', id: message.id, state, runtimeVersion:state==='running'?await runtimeVersion():null }); console.log(({ received: 'Déploiement validé.', installing: 'Préparation du bot.', running: 'Déploiement terminé. Bot démarré.', failed: 'Échec du déploiement.' })[state] || 'État du déploiement modifié.'); }); }
|
|
206
210
|
catch { send({ type: 'deployment-status', id: message.id, state: 'failed' }); console.error('Déploiement refusé ou échoué.'); }
|
|
207
211
|
}
|
|
208
212
|
}).catch(() => console.error('Message refusé.'));
|
|
@@ -232,7 +236,7 @@ function printHelp() {
|
|
|
232
236
|
status Afficher l’état local
|
|
233
237
|
logs Lire les 100 dernières lignes du journal
|
|
234
238
|
token [--stdin] Régénérer l’authentification agent ; --stdin lit 64 caractères hexadécimaux
|
|
235
|
-
remove --yes
|
|
239
|
+
remove | delete --yes Supprimer une instance arrêtée de la liste et révoquer sa liaison
|
|
236
240
|
list Afficher les instances installées
|
|
237
241
|
help Afficher cette aide
|
|
238
242
|
Options : --bot=IDENTIFIANT_COURT (voir list) pour choisir un bot ; start --foreground pour diagnostiquer.
|
|
@@ -241,7 +245,7 @@ Documentation : https://doc-builder.oftone.fr/agent`);
|
|
|
241
245
|
}
|
|
242
246
|
async function main() {
|
|
243
247
|
const [requestedCommand, ...args] = process.argv.slice(2);
|
|
244
|
-
const command = requestedCommand || 'login';
|
|
248
|
+
const command = requestedCommand==='delete'?'remove':requestedCommand || 'login';
|
|
245
249
|
const source = path.dirname(fileURLToPath(import.meta.url));
|
|
246
250
|
if (['help', '--help', '-h'].includes(command)) { printHelp(); return; }
|
|
247
251
|
if (command === 'list') { for (const instance of await listInstances(baseHome)) console.log(`${instance.agentId.slice(0, 8)} · ${(await localStatus(instance.home)).running ? 'actif' : 'arrêté'}`); return; }
|
|
@@ -326,7 +330,8 @@ async function main() {
|
|
|
326
330
|
}
|
|
327
331
|
if (command === 'remove') {
|
|
328
332
|
if (!args.includes('--yes')) { console.log('Supprime l’agent sélectionné et ses données locales, y compris son token Discord. Relancez avec remove --yes pour confirmer.'); return; }
|
|
329
|
-
await
|
|
333
|
+
const state=await localStatus(home);if(state.running||state.starting||state.legacy)throw Error('REMOVE_RUNNING');
|
|
334
|
+
const config = JSON.parse(await readFile(configPath, 'utf8'));
|
|
330
335
|
let revoked = false;
|
|
331
336
|
try { const response = await fetch(`${config.relay}/v1/agents/${config.object}/remove`, { method: 'POST', headers: { Authorization: `Bearer ${config.credential}` }, signal: AbortSignal.timeout(15000) }); revoked = response.ok; } catch {}
|
|
332
337
|
await removeLocalInstance(home, baseHome, config);
|
|
@@ -334,5 +339,5 @@ async function main() {
|
|
|
334
339
|
}
|
|
335
340
|
throw new Error('COMMAND_UNKNOWN');
|
|
336
341
|
}
|
|
337
|
-
main().catch(error => { const messages = { LOGIN_BUSY: 'Une connexion Discord est déjà en cours sur cette machine.', LOGIN_EXPIRED: 'La demande a expiré. Relancez npx oft-agt login.', LOGIN_START_FAILED: 'Impossible de démarrer la connexion. Réessayez dans quelques minutes.', LOGIN_FAILED: 'Connexion refusée. Relancez npx oft-agt login.', AUTH_ROTATION_BUSY: 'Une modification de l’authentification est déjà en cours pour cette instance. Réessayez après sa fin.', COMMAND_UNKNOWN: 'Commande inconnue. Consultez oftone-agent help.', LEGACY_FOREGROUND: 'Un ancien agent fonctionne au premier plan. Arrêtez-le avec Ctrl+C une seule fois, puis utilisez start.', DAEMON_START_FAILED: 'L’agent n’a pas démarré. Consultez logs.', DAEMON_START_TIMEOUT: 'Démarrage non confirmé. Consultez status et logs avant de réessayer.', DAEMON_STOP_TIMEOUT: 'L’arrêt n’est pas encore confirmé. Consultez status et logs.', AUTH_ROTATION_FAILED: 'Changement d’authentification non confirmé. Réessayez token ou start pour reprendre sans perdre la liaison.', AUTH_ROTATION_INVALID: 'Token d’authentification invalide : 64 caractères hexadécimaux attendus.', PAIRING_INVALID: 'Commande de liaison invalide. Copiez à nouveau la commande complète depuis le dashboard.', PAIRING_REJECTED: 'Commande de liaison expirée, déjà utilisée ou révoquée. Générez-en une nouvelle depuis le bot concerné.', INSTANCE_INVALID: 'Identifiant d’instance invalide. Copiez la commande depuis les paramètres du bot concerné.', INSTANCE_NOT_FOUND: 'Cette liaison n’est pas installée sur cette machine. Utilisez la commande locale de cette instance ou configurez une nouvelle liaison.', INSTANCE_REQUIRED: 'Précisez l’instance avec la commande affichée dans les paramètres du bot ou la commande locale affichée à l’installation.', INSTANCE_EXISTS: 'Cette instance existe déjà. Utilisez sa commande locale de démarrage.', AGENT_ACTIVE: 'Cet agent est déjà actif. Arrêtez uniquement le processus de cette instance avant de le relancer.' }; console.error(messages[error.code || error.message] || 'Échec de l’agent. Vérifiez la liaison et la configuration locale. Aucun secret affiché.'); process.exitCode = 1; });
|
|
342
|
+
main().catch(error => { const messages = { REMOVE_RUNNING:'Arrêtez ce bot avec stop avant de le supprimer de la liste.', LOGIN_BUSY: 'Une connexion Discord est déjà en cours sur cette machine.', LOGIN_EXPIRED: 'La demande a expiré. Relancez npx oft-agt login.', LOGIN_START_FAILED: 'Impossible de démarrer la connexion. Réessayez dans quelques minutes.', LOGIN_FAILED: 'Connexion refusée. Relancez npx oft-agt login.', AUTH_ROTATION_BUSY: 'Une modification de l’authentification est déjà en cours pour cette instance. Réessayez après sa fin.', COMMAND_UNKNOWN: 'Commande inconnue. Consultez oftone-agent help.', LEGACY_FOREGROUND: 'Un ancien agent fonctionne au premier plan. Arrêtez-le avec Ctrl+C une seule fois, puis utilisez start.', DAEMON_START_FAILED: 'L’agent n’a pas démarré. Consultez logs.', DAEMON_START_TIMEOUT: 'Démarrage non confirmé. Consultez status et logs avant de réessayer.', DAEMON_STOP_TIMEOUT: 'L’arrêt n’est pas encore confirmé. Consultez status et logs.', AUTH_ROTATION_FAILED: 'Changement d’authentification non confirmé. Réessayez token ou start pour reprendre sans perdre la liaison.', AUTH_ROTATION_INVALID: 'Token d’authentification invalide : 64 caractères hexadécimaux attendus.', PAIRING_INVALID: 'Commande de liaison invalide. Copiez à nouveau la commande complète depuis le dashboard.', PAIRING_REJECTED: 'Commande de liaison expirée, déjà utilisée ou révoquée. Générez-en une nouvelle depuis le bot concerné.', INSTANCE_INVALID: 'Identifiant d’instance invalide. Copiez la commande depuis les paramètres du bot concerné.', INSTANCE_NOT_FOUND: 'Cette liaison n’est pas installée sur cette machine. Utilisez la commande locale de cette instance ou configurez une nouvelle liaison.', INSTANCE_REQUIRED: 'Précisez l’instance avec la commande affichée dans les paramètres du bot ou la commande locale affichée à l’installation.', INSTANCE_EXISTS: 'Cette instance existe déjà. Utilisez sa commande locale de démarrage.', AGENT_ACTIVE: 'Cet agent est déjà actif. Arrêtez uniquement le processus de cette instance avant de le relancer.' }; console.error(messages[error.code || error.message] || 'Échec de l’agent. Vérifiez la liaison et la configuration locale. Aucun secret affiché.'); process.exitCode = 1; });
|
|
338
343
|
|
package/compile-plan.mjs
CHANGED
|
@@ -46,9 +46,9 @@ export function compile(input) {
|
|
|
46
46
|
if (!plain(command)) fail(`Commande ${commandIndex + 1} : configuration absente.`);
|
|
47
47
|
|
|
48
48
|
validateEventGraph(command);
|
|
49
|
-
const name = text(command.name ?? entry.name, 'Nom
|
|
49
|
+
const name = text(command.name ?? entry.name, 'Nom', command.trigger?.kind==='event'?100:32, true);
|
|
50
50
|
|
|
51
|
-
if (!/^[a-z0-9_-]{1,32}$/.test(name)) fail(`/${name} : utilisez des minuscules, chiffres, tirets ou underscores.`);
|
|
51
|
+
if (command.trigger?.kind!=='event' && !/^[a-z0-9_-]{1,32}$/.test(name)) fail(`/${name} : utilisez des minuscules, chiffres, tirets ou underscores.`);
|
|
52
52
|
|
|
53
53
|
if (names.has(name)) fail(`Commande dupliquée : /${name}.`);
|
|
54
54
|
|
package/compiler.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import {RUNTIME_VERSION} from './runtime/version.mjs';
|
|
2
3
|
import { readFile, writeFile, mkdir, cp, access } from 'node:fs/promises';
|
|
3
4
|
import path from 'node:path';
|
|
4
5
|
import { fileURLToPath } from 'node:url';
|
|
@@ -16,6 +17,7 @@ export async function writeBot(input, outputDirectory) {
|
|
|
16
17
|
await mkdir(output, { recursive: false });
|
|
17
18
|
await cp(path.join(ROOT, 'runtime'), path.join(output, 'runtime'), { recursive: true });
|
|
18
19
|
await cp(path.join(ROOT, 'templates/bot.mjs'), path.join(output, 'bot.mjs'));
|
|
20
|
+
await writeFile(path.join(output,'runtime-version.json'),JSON.stringify({version:RUNTIME_VERSION}));
|
|
19
21
|
await writeFile(path.join(output, 'plan.json'), JSON.stringify(plan, null, 2) + '\n');
|
|
20
22
|
await writeFile(path.join(output, 'package.json'), JSON.stringify({ name: 'oftone-generated-bot', version: '1.0.0', private: true, type: 'module', engines: { node: '>=22.5.0' }, scripts: { start: 'node --env-file=.env bot.mjs', deploy: 'node --env-file=.env bot.mjs --deploy' }, dependencies: { 'discord.js': '^14.14.0' } }, null, 2) + '\n');
|
|
21
23
|
await writeFile(path.join(output, '.env.example'), '# Token saisi uniquement sur votre serveur.\nDISCORD_TOKEN=\n# Application ID Discord (pas l’identifiant du projet Builder).\nDISCORD_CLIENT_ID=\n# Optionnel : serveur de test. Vide = commandes globales.\nDISCORD_GUILD_ID=\n');
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "oft-agt",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.17",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "oft-agt",
|
|
9
|
-
"version": "0.5.
|
|
9
|
+
"version": "0.5.17",
|
|
10
|
+
"license": "UNLICENSED",
|
|
10
11
|
"dependencies": {
|
|
11
12
|
"discord.js": "^14.14.0",
|
|
12
13
|
"ws": "^8.18.0"
|
|
@@ -17,19 +18,18 @@
|
|
|
17
18
|
},
|
|
18
19
|
"engines": {
|
|
19
20
|
"node": ">=22.5"
|
|
20
|
-
}
|
|
21
|
-
"license": "UNLICENSED"
|
|
21
|
+
}
|
|
22
22
|
},
|
|
23
23
|
"node_modules/@discordjs/builders": {
|
|
24
|
-
"version": "1.
|
|
25
|
-
"resolved": "https://registry.npmjs.org/@discordjs/builders/-/builders-1.
|
|
26
|
-
"integrity": "sha512-
|
|
24
|
+
"version": "1.15.0",
|
|
25
|
+
"resolved": "https://registry.npmjs.org/@discordjs/builders/-/builders-1.15.0.tgz",
|
|
26
|
+
"integrity": "sha512-AifaDWTw6WUDhKBn8E6mwwoyjDAcfsYq+ZE0nKSu1dMH1NLR22D3hGkNx+ctEv8NqHEHdAZnP5REteHDmn1lkA==",
|
|
27
27
|
"license": "Apache-2.0",
|
|
28
28
|
"dependencies": {
|
|
29
29
|
"@discordjs/formatters": "^0.6.2",
|
|
30
30
|
"@discordjs/util": "^1.2.0",
|
|
31
31
|
"@sapphire/shapeshift": "^4.0.0",
|
|
32
|
-
"discord-api-types": "^0.38.
|
|
32
|
+
"discord-api-types": "^0.38.50",
|
|
33
33
|
"fast-deep-equal": "^3.1.3",
|
|
34
34
|
"ts-mixer": "^6.0.4",
|
|
35
35
|
"tslib": "^2.6.3"
|
|
@@ -218,19 +218,19 @@
|
|
|
218
218
|
"license": "MIT"
|
|
219
219
|
},
|
|
220
220
|
"node_modules/discord.js": {
|
|
221
|
-
"version": "14.
|
|
222
|
-
"resolved": "https://registry.npmjs.org/discord.js/-/discord.js-14.
|
|
223
|
-
"integrity": "sha512-
|
|
221
|
+
"version": "14.28.1",
|
|
222
|
+
"resolved": "https://registry.npmjs.org/discord.js/-/discord.js-14.28.1.tgz",
|
|
223
|
+
"integrity": "sha512-Je5OcqgurnIUQK4vR+gQnmzOf/lNQsD6d/UkV8ZOj+bFa5zJXPyiGXlbywp2EagwshVT18y7YNQNSRd3FjYkDw==",
|
|
224
224
|
"license": "Apache-2.0",
|
|
225
225
|
"dependencies": {
|
|
226
|
-
"@discordjs/builders": "^1.
|
|
226
|
+
"@discordjs/builders": "^1.15.0",
|
|
227
227
|
"@discordjs/collection": "1.5.3",
|
|
228
228
|
"@discordjs/formatters": "^0.6.2",
|
|
229
|
-
"@discordjs/rest": "^2.6.
|
|
229
|
+
"@discordjs/rest": "^2.6.3",
|
|
230
230
|
"@discordjs/util": "^1.2.0",
|
|
231
231
|
"@discordjs/ws": "^1.2.3",
|
|
232
232
|
"@sapphire/snowflake": "3.5.5",
|
|
233
|
-
"discord-api-types": "^0.38.
|
|
233
|
+
"discord-api-types": "^0.38.50",
|
|
234
234
|
"fast-deep-equal": "3.1.3",
|
|
235
235
|
"lodash.snakecase": "4.1.1",
|
|
236
236
|
"magic-bytes.js": "^1.13.0",
|
|
@@ -296,9 +296,9 @@
|
|
|
296
296
|
"license": "MIT"
|
|
297
297
|
},
|
|
298
298
|
"node_modules/ws": {
|
|
299
|
-
"version": "8.22.
|
|
300
|
-
"resolved": "https://registry.npmjs.org/ws/-/ws-8.22.
|
|
301
|
-
"integrity": "sha512-
|
|
299
|
+
"version": "8.22.1",
|
|
300
|
+
"resolved": "https://registry.npmjs.org/ws/-/ws-8.22.1.tgz",
|
|
301
|
+
"integrity": "sha512-oyL6OPYme/CmiFvNuGzeoxCJ4nOrZE2O1FwM9MWM1AsGri6nLXB2pg+lNA8aLeXMSav0J6eIVS1j6vUPzUtK9g==",
|
|
302
302
|
"license": "MIT",
|
|
303
303
|
"engines": {
|
|
304
304
|
"node": ">=10.0.0"
|
package/package.json
CHANGED
package/runtime/api-request.mjs
CHANGED
|
@@ -4,13 +4,20 @@ import https from 'node:https';
|
|
|
4
4
|
import {lookup} from 'node:dns';
|
|
5
5
|
import {isIP} from 'node:net';
|
|
6
6
|
import {validateApiRequest} from './api-config.mjs';
|
|
7
|
-
export function publicAddress(ip){
|
|
7
|
+
export function publicAddress(ip){
|
|
8
|
+
if(isIP(ip)===6){const parts=ip.toLowerCase().split(':'),first=parseInt(parts[0],16),second=parseInt(parts[1]||'0',16);return first>=0x2000&&first<=0x3fff&&first!==0x2002&&first!==0x3fff&&!(first===0x2001&&(second<0x200||second===0xdb8));}
|
|
9
|
+
if(isIP(ip)!==4)return false;
|
|
10
|
+
const [a,b,c]=ip.split('.').map(Number);
|
|
11
|
+
return a>0&&a<224&&![10,127].includes(a)&&!(a===169&&b===254)&&!(a===172&&b>=16&&b<=31)&&!(a===192&&(b===168||b===0&&(c===0||c===2)||b===88&&c===99))&&!(a===100&&b>=64&&b<=127)&&!(a===198&&(b===18||b===19||b===51&&c===100))&&!(a===203&&b===0&&c===113);
|
|
12
|
+
}
|
|
8
13
|
export async function requestApi(raw,{resolveSecret=async()=>{throw Error('Secret indisponible.');},signal}={}){
|
|
9
14
|
const resolvedSecrets=[];const c=validateApiRequest(raw),replace=async s=>{let out=s;for(const m of s.matchAll(/\{secret\.([A-Za-z][A-Za-z0-9_]*)\}/g)){const value=await resolveSecret(m[1]);resolvedSecrets.push(value);out=out.replaceAll(m[0],value);}return out;};
|
|
10
15
|
const url=new URL(await replace(c.url));if(url.username||url.password||!['http:','https:'].includes(url.protocol))throw Error('URL refusée.');
|
|
11
16
|
for(const p of c.params)if(p.key)url.searchParams.append(p.key,await replace(p.value));
|
|
12
17
|
const headers={'User-Agent':'Oftone-Agent/1'};for(const h of c.headers)if(h.key){if(/^(host|content-length|connection|transfer-encoding|upgrade)$/i.test(h.key)||/[\r\n]/.test(h.key))throw Error('En-tête refusé.');headers[h.key]=await replace(h.value);}
|
|
13
|
-
|
|
18
|
+
let body;let bodyJson;try{bodyJson=JSON.parse(c.body);}catch{}
|
|
19
|
+
if(bodyJson!==undefined){const resolve=async value=>typeof value==='string'?replace(value):Array.isArray(value)?Promise.all(value.map(resolve)):value&&typeof value==='object'?Object.fromEntries(await Promise.all(Object.entries(value).map(async([k,v])=>[k,await resolve(v)]))):value;body=JSON.stringify(await resolve(bodyJson));}else body=await replace(c.body);
|
|
20
|
+
if(Buffer.byteLength(body)>65536)throw Error('Corps trop volumineux.');if(resolvedSecrets.length&&url.protocol!=='https:')throw Error('HTTPS requis pour utiliser un secret.');
|
|
14
21
|
const result=await new Promise((resolve,reject)=>{
|
|
15
22
|
if(isIP(url.hostname.replaceAll(/[\[\]]/g,''))&&!publicAddress(url.hostname.replaceAll(/[\[\]]/g,'')))return reject(Error('Adresse locale refusée.'));
|
|
16
23
|
const req=(url.protocol==='https:'?https:http).request(url,{method:c.method,headers,signal:signal?AbortSignal.any([signal,AbortSignal.timeout(c.timeout)]):AbortSignal.timeout(c.timeout),lookup:(host,options,cb)=>lookup(host,{all:true},(err,addresses)=>{if(err||!addresses.length||addresses.some(a=>!publicAddress(a.address)))return cb(Error('Adresse réseau refusée.'));const a=addresses[0];cb(null,options.all?[a]:a.address,a.family);})},res=>{let size=0;const chunks=[];res.on('data',chunk=>{size+=chunk.length;if(size>262144){req.destroy(Error('Réponse trop volumineuse.'));return;}chunks.push(chunk);});res.on('end',()=>{const text=Buffer.concat(chunks).toString('utf8');let response;try{response=JSON.parse(text);}catch{response=text;}resolve({status:res.statusCode,statusText:res.statusMessage||'',response});});res.on('error',reject);});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const RUNTIME_VERSION = '0.5.17';
|
package/usage.mjs
CHANGED
|
@@ -12,7 +12,7 @@ export async function createUsageCollector(home, now = () => Date.now()) {
|
|
|
12
12
|
let writing = Promise.resolve();
|
|
13
13
|
return {
|
|
14
14
|
record(event) {
|
|
15
|
-
if (event?.type !== 'usage' ||
|
|
15
|
+
if (event?.type !== 'usage' || (typeof event.command!=='string'||!event.command.trim()||event.command.length>100||/[\u0000-\u001f\u007f]/.test(event.command)) || typeof event.success !== 'boolean' || !Number.isFinite(event.durationMs) || event.durationMs < 0 || event.durationMs > 900000) return;
|
|
16
16
|
const current = Math.floor(now() / HOUR) * HOUR;
|
|
17
17
|
const hour = event.hour ?? current;
|
|
18
18
|
if (!Number.isSafeInteger(hour) || hour % HOUR || hour > current || hour < current - 719 * HOUR) return;
|
package/version.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const AGENT_VERSION = '0.5.
|
|
1
|
+
export const AGENT_VERSION = '0.5.17';
|
package/README.md
DELETED
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
# Oftone Server Agent
|
|
2
|
-
|
|
3
|
-
Agent Node.js 22+ de liaison avec le dashboard. Le relais est hébergé sur Cloudflare Workers avec un Durable Object SQLite ; les bots Discord s’exécutent sur le serveur utilisateur.
|
|
4
|
-
|
|
5
|
-
Générez la commande d’installation depuis **Liaison du serveur** dans le dashboard. Elle utilise un token à usage unique de dix minutes, puis l’agent dispose de sa propre identité. Exécutez-la avec un compte système dédié, sans privilèges administrateur.
|
|
6
|
-
|
|
7
|
-
Les fichiers privés et l’installation persistante se trouvent par défaut dans `~/.oftone-agent`. La variable `OFTONE_AGENT_HOME` permet de choisir un autre dossier, notamment pour héberger un deuxième bot. Ne partagez jamais le dossier privé entre utilisateurs.
|
|
8
|
-
|
|
9
|
-
Après installation, l’agent affiche la commande locale pour redémarrer sans télécharger le paquet :
|
|
10
|
-
|
|
11
|
-
```text
|
|
12
|
-
node <dossier-utilisateur>/.oftone-agent/installation/cli.mjs run
|
|
13
|
-
```
|
|
14
|
-
|
|
15
|
-
Le processus doit rester actif. Cette version n’installe pas de service système de démarrage automatique. Une coupure du réseau Cloudflare n’arrête pas le bot déjà lancé.
|
|
16
|
-
|
|
17
|
-
Le token Discord est configuré depuis le dashboard après liaison, chiffré pour la clé publique de cet agent et conservé localement sous forme d’enveloppe chiffrée. La clé privée locale permet au processus de le déchiffrer pour lancer le bot. Les permissions du compte système restent essentielles ; sous Windows, vérifier les ACL du dossier.
|
|
18
|
-
|
|
19
|
-
Le bouton **Déployer** transmet un snapshot des commandes enregistrées. L’agent valide ce JSON, le compile avec les fonctions embarquées et démarre une version locale. Aucun JavaScript, script shell ou paquet npm fourni par le JSON n’est exécuté. Les dépendances proviennent du paquet de l’agent, verrouillées par `npm-shrinkwrap.json`.
|
|
20
|
-
|
|
21
|
-
La commande de déploiement remplace les slash commands de l’application Discord : utiliser une application dédiée. Les changements de token et la rotation de l’identité de l’agent restent à compléter. Les processus des bots ne sont pas encore isolés dans des conteneurs.
|
|
22
|
-
|
|
23
|
-
Pour reconstruire le paquet : `npm pack --ignore-scripts`. Le dossier du builder contient `server-link/README.md`, les tests du relais et la configuration Wrangler. Le paquet public est servi par le Worker ; aucun envoi au registre npm n’est nécessaire.
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
## Version 0.5.2
|
|
27
|
-
|
|
28
|
-
Les envois vers un salon vérifient désormais le serveur et les droits de
|
|
29
|
-
l’utilisateur, ainsi que l’appartenance aux fils privés. Le moteur adapte sa
|
|
30
|
-
concurrence à la charge CPU globale du PC, à la mémoire disponible et à sa
|
|
31
|
-
réactivité. Les pauses libèrent une place de traitement ; la file et les
|
|
32
|
-
invocations conservées restent bornées pour protéger l’hébergement.
|
|
33
|
-
|
|
34
|
-
Installation : `npm install -g oft-agt` (Node.js 22 ou supérieur).
|
|
35
|
-
La mise à jour du paquet npm ne remplace pas automatiquement les installations
|
|
36
|
-
persistantes déjà liées dans le dossier privé de l’agent.
|