/**
* Deploy service — managed wrapper around hyper-deploy's DeployServer.
*
* hyper-deploy owns its own lifecycle (ready/close) and can accept an
* injected Hyperswarm. The orchestrator passes its existing swarm so we
* don't spin up a second DHT node. The release directory at
* `<storage>/releases/` is the source of truth — files placed there are
* indexed and served to peers.
*
* Only started when the orchestrator has swarm enabled (opts.swarm is set).
* Without swarm, hyper-deploy has nothing to serve to.
*/
import { DeployServer } from 'hyper-deploy'
import { mkdir } from '#node:fs/promises'
import { readdirSync, statSync } from '#node:fs'
import { join } from '#node:path'
export async function startDeployService (opts) {
const { releaseDir, swarm, corestore, identity } = opts
if (!releaseDir) throw new Error('deploy service requires releaseDir')
if (!swarm) throw new Error('deploy service requires a Hyperswarm instance')
await mkdir(releaseDir, { recursive: true })
const server = new DeployServer(releaseDir, {
swarm,
corestore: corestore || undefined,
topic: identity ? identity.publicKey : undefined
})
// DeployServer re-emits connection errors (ECONNRESET etc.) as 'error'
// events. These are routine in P2P and must not crash the daemon.
// Register BEFORE ready() because the constructor auto-starts.
server.on('error', (err) => {
// eslint-disable-next-line no-console
if (err.code !== 'ECONNRESET') console.error('[deploy]', err.message)
})
await server.ready()
return server
}
/**
* RPC handlers for the deploy service.
* Read-only — release management happens via the filesystem + CI, not RPC.
*/
export function deployHandlers (svc) {
return [
{
name: 'list-releases',
fn: async () => {
const deploy = svc._deploy
if (!deploy) return { files: [], active: false }
return {
active: true,
topic: deploy.topicHex,
connections: deploy.connections,
files: deploy.manifest ? deploy.manifest.files : [],
totalSize: deploy.manifest ? deploy.manifest.totalSize : 0
}
}
},
{
name: 'release-status',
fn: async () => {
const deploy = svc._deploy
if (!deploy) return { active: false }
return {
active: true,
topic: deploy.topicHex,
connections: deploy.connections,
stats: deploy.stats,
fileCount: deploy.manifest ? deploy.manifest.files.length : 0,
totalSize: deploy.manifest ? deploy.manifest.totalSize : 0
}
}
},
{
name: 'rescan-releases',
fn: async (ctx) => {
// requireWriter equivalent — UDS or trusted HTTP
if (ctx.transport !== 'uds' && (!ctx.identity || !ctx.identity.publicKey)) {
throw Object.assign(new Error('authentication required'), { code: 'EAUTH', status: 401 })
}
const deploy = svc._deploy
if (!deploy) throw new Error('deploy service not active (no swarm)')
const result = await deploy.rescan()
return result
}
}
]
}