#!/usr/bin/env node 'use strict'; const crypto = require('crypto'); const path = require('path'); const { pathToFileURL } = require('url'); const { readQiweiGuid } = require('../mcp/src/core/credentials'); const { getRelayBaseUrl, getTenantApiSecret, getRelayPrivateKey, getTenantId, getRelayDeviceGuid, } = require('../mcp/src/core/relay-config'); function runtimeModuleUrl(relativePath) { return pathToFileURL(path.join(__dirname, '..', 'runtime', 'callback-service', 'src', relativePath)).href; } function decryptPayload(encryptedPayload, privateKeyPem) { const key = privateKeyPem && typeof privateKeyPem === 'object' && privateKeyPem.type === 'private' ? privateKeyPem : crypto.createPrivateKey(privateKeyPem); if (String(encryptedPayload).startsWith('v2:')) { const envelope = JSON.parse(Buffer.from(String(encryptedPayload).slice(3), 'base64').toString('utf8')); const aesKey = crypto.privateDecrypt( { key, oaepHash: 'sha256' }, Buffer.from(envelope.key, 'base64'), ); const decipher = crypto.createDecipheriv('aes-256-gcm', aesKey, Buffer.from(envelope.iv, 'base64')); decipher.setAuthTag(Buffer.from(envelope.tag, 'base64')); return Buffer.concat([ decipher.update(Buffer.from(envelope.ciphertext, 'base64')), decipher.final(), ]).toString('utf8'); } return crypto.privateDecrypt( { key, oaepHash: 'sha256' }, Buffer.from(encryptedPayload, 'base64'), ).toString('utf8'); } async function ackEvents(baseUrl, apiSecret, guid, eventIds) { if (!eventIds.length) return 0; const response = await fetch(`${String(baseUrl).replace(/\/$/, '')}/api/relay/ack`, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${apiSecret}`, }, body: JSON.stringify({ guid, eventIds }), }); if (!response.ok) throw new Error(`ACK failed: ${response.status} ${await response.text()}`); const data = await response.json(); return Number(data.ackedCount) || eventIds.length; } function resolveGuid() { return process.argv[2] || process.env.RELAY_DEVICE_GUID || getRelayDeviceGuid() || readQiweiGuid() || ''; } function resolveRuntimeConfig() { return { baseUrl: getRelayBaseUrl(), apiSecret: getTenantApiSecret(), privateKey: getRelayPrivateKey(), tenantId: getTenantId(), guid: resolveGuid(), }; } async function runPollOnce(baseUrl, apiSecret, guid, privateKey) { const { EnterpriseRelayRuntime } = await import(runtimeModuleUrl('enterprise-relay-client.mjs')); const runtime = new EnterpriseRelayRuntime({ config: { batchSize: 100, waitMs: 30000, retryMinMs: 1000, retryMaxMs: 60000, }, }); const result = await runtime.pollOnce({ baseUrl, apiSecret, guid, privateKey, tenantId: 'compat' }, { throwOnFailure: false, }); return { received: result.received, acked: result.acked, failed: result.failed, }; } async function main() { const { startRuntime } = await import(runtimeModuleUrl('index.mjs')); const runtime = await startRuntime({ forceMode: 'enterprise', guid: process.argv[2] || '', dashboard: false, }); process.stdout.write(`[RelayClient] ESM runtime started. pid=${process.pid}, mode=${runtime.mode}\n`); let shuttingDown = false; const shutdown = signal => { if (shuttingDown) return; shuttingDown = true; runtime.stop(signal).finally(() => process.exit(0)); }; process.once('SIGINT', () => shutdown('SIGINT')); process.once('SIGTERM', () => shutdown('SIGTERM')); } if (require.main === module) { main().catch(error => { process.stderr.write(`[RelayClient] ${error.message}\n`); process.exit(1); }); } module.exports = { decryptPayload, ackEvents, runPollOnce, resolveGuid, resolveRuntimeConfig, };