OverviewFilesCommitsIssuesDocsReleasesPeers

pear-git-v3 / lib / handlers / follows.js

120 lines · javascript ref:
/**
 * Follow handlers — consume side of follower mode.
 *
 * RPCs:
 *   follow-repo({ payload | url })      → start a RepoFollower, persist
 *   unfollow-repo({ sourceServerKey, name })
 *                                         → stop a RepoFollower, persist removal
 *                                          (on-disk cores + repo bytes preserved)
 *   list-repo-follows()                 → snapshot of active follows
 *
 * Org variants come in Phase 2.
 *
 * Auth:
 *   follow-repo, unfollow-repo  — UDS only (operator intent, write).
 *   list-repo-follows           — UDS + swarm + HTTP read OK.
 */

import b4a from 'b4a'

import { decodeB64, parseUrl } from '../services/follow-payload.js'

function requireUds (ctx, rpc) {
  if (!ctx || ctx.transport !== 'uds') {
    throw Object.assign(new Error(rpc + ' requires UDS transport'), {
      code: 'EAUTH', status: 403
    })
  }
}

export function followHandlers (svc) {
  return [
    {
      name: 'follow-repo',
      fn: async (ctx, params = {}) => {
        requireUds(ctx, 'follow-repo')

        if (!svc._follows || !svc._follows.startRepoFollow) {
          throw new Error('follower mode not initialized on this daemon')
        }

        let payload
        if (params.payload) {
          payload = decodeB64(params.payload)
        } else if (params.url) {
          // URL form ships only (srvKey, kind, name) — we have to fetch
          // the full payload from the source over swarm-RPC. The
          // helper does the swarm-dial + call. If unimplemented (this
          // daemon can't reach the source's RPC), surface a clean
          // error directing the user to paste the binary payload form.
          payload = await svc._follows.fetchPayloadFromUrl(params.url)
        } else {
          throw new Error('follow-repo requires { payload } or { url }')
        }
        if (!payload || payload.kind !== 'repo') {
          throw new Error('follow-repo requires a repo-kind payload (got ' +
            (payload && payload.kind) + ')')
        }

        // Conflict guards
        // (a) If this daemon is a mirror of the same source, don't follow —
        //     mirror flow already replicates everything.
        if (svc.originCfg && svc.originCfg.serverKey &&
            String(svc.originCfg.serverKey).toLowerCase() === payload.sourceServerKey) {
          return {
            alreadyMirror: true,
            sourceServerKey: payload.sourceServerKey,
            name: payload.name,
            message: 'daemon is already a mirror of this source — no follow needed'
          }
        }

        const result = await svc._follows.startRepoFollow(payload)
        return {
          ok: true,
          sourceServerKey: payload.sourceServerKey,
          name: payload.name,
          ...result
        }
      }
    },

    {
      name: 'unfollow-repo',
      fn: async (ctx, params = {}) => {
        requireUds(ctx, 'unfollow-repo')
        if (!params.sourceServerKey || !params.name) {
          throw new Error('unfollow-repo requires { sourceServerKey, name }')
        }
        if (!svc._follows) {
          throw new Error('follower mode not initialized on this daemon')
        }
        const removed = await svc._follows.stopRepoFollow(
          String(params.sourceServerKey).toLowerCase(),
          params.name
        )
        return { ok: true, removed, sourceServerKey: params.sourceServerKey, name: params.name }
      }
    },

    {
      name: 'list-repo-follows',
      fn: async () => {
        if (!svc._follows) return { active: false, follows: [] }
        return { active: true, follows: svc._follows.listRepoFollows() }
      }
    }
  ]
}

/** Helper: best-effort decode of a payload string from either form. */
export function decodeFollowToken (token) {
  if (!token) throw new Error('decodeFollowToken requires a token')
  // URL form is recognisable by the punch+follow:// prefix.
  if (typeof token === 'string' && token.startsWith('punch+follow://')) {
    return { kind: 'url', url: token, parsed: parseUrl(token) }
  }
  // Otherwise treat as a b64 payload (caller can decode it).
  return { kind: 'payload', payload: token }
}