oft-agt 0.5.15 → 0.5.16

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 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); }
@@ -161,7 +162,7 @@ async function start(config) {
161
162
  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
163
  } catch {}
163
164
  }
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 });
165
+ 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
166
  if (process.connected) process.send?.({ type: 'agent-ready' }, () => {});
166
167
  await usage.flush(send).catch(() => console.error('Statistiques locales indisponibles.'));
167
168
  usageTimer = setInterval(() => usage.flush(send).catch(() => console.error('Statistiques locales indisponibles.')), 60000);
@@ -202,7 +203,7 @@ async function start(config) {
202
203
  if (message.type === 'deploy') {
203
204
  console.log(`[${new Date().toISOString()}] Nouveau déploiement reçu.`);
204
205
  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é.'); }); }
206
+ try { await deploy(config, message, async state => { 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
207
  catch { send({ type: 'deployment-status', id: message.id, state: 'failed' }); console.error('Déploiement refusé ou échoué.'); }
207
208
  }
208
209
  }).catch(() => console.error('Message refusé.'));
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');
@@ -1,12 +1,13 @@
1
1
  {
2
2
  "name": "oft-agt",
3
- "version": "0.5.15",
3
+ "version": "0.5.16",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "oft-agt",
9
- "version": "0.5.15",
9
+ "version": "0.5.16",
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.14.1",
25
- "resolved": "https://registry.npmjs.org/@discordjs/builders/-/builders-1.14.1.tgz",
26
- "integrity": "sha512-gSKkhXLqs96TCzk66VZuHHl8z2bQMJFGwrXC0f33ngK+FLNau4hU1PYny3DNJfNdSH+gVMzE85/d5FQ2BpcNwQ==",
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.40",
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.27.0",
222
- "resolved": "https://registry.npmjs.org/discord.js/-/discord.js-14.27.0.tgz",
223
- "integrity": "sha512-qHbFlFG2N7y3LjPySYsL6A1+BnX6bkTVgo842EX0CqVPk/KTMwZkojPHEXKsQUpWZNyz5BISNHK1cPpQw0+m4A==",
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.14.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.2",
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.49",
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.0",
300
- "resolved": "https://registry.npmjs.org/ws/-/ws-8.22.0.tgz",
301
- "integrity": "sha512-Ydggc987+RO0AnWtZ/7Wq9FtNvcrL1b/RO0ud9mWjUPgDrsAAwQSF51sm2hm1XofbU/4jkpGEsLFsZZxU+1DOg==",
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oft-agt",
3
- "version": "0.5.15",
3
+ "version": "0.5.16",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "engines": {
@@ -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){if(ip.includes(':'))return /^2[0-9a-f]{3}:/i.test(ip)&&!/^2001:(db8|0):/i.test(ip);const a=ip.split('.').map(Number);return a.length===4&&a[0]>0&&a[0]<224&&![10,127].includes(a[0])&&!(a[0]===169&&a[1]===254)&&!(a[0]===172&&a[1]>=16&&a[1]<=31)&&!(a[0]===192&&a[1]===168)&&!(a[0]===100&&a[1]>=64&&a[1]<=127);}
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
- const body=await replace(c.body);if(resolvedSecrets.length&&url.protocol!=='https:')throw Error('HTTPS requis pour utiliser un secret.');
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.16';
package/version.mjs CHANGED
@@ -1 +1 @@
1
- export const AGENT_VERSION = '0.5.15';
1
+ export const AGENT_VERSION = '0.5.16';
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.