"use strict";

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.toConfigs = exports.nodesByIp = exports.makeEmptyContext = exports.ipForNode = exports.generateVpnTunneledAccess = exports.generateStarConnections = exports.generateServerToServerAccess = exports.generateServerHubAndSpokeAccess = exports.generateRemoteTunneledAccess = exports.generateRemoteAccessToServer = exports.generateRemoteAccessToLan = exports.generateLanToLanAccess = exports.generateLanHubAndSpokeAccess = exports.generateKeys = exports.generateHubAndSpokeConnections = exports.computeAllowedIPsFromConnections = exports.addPreshareKeys = exports.addConnection = exports.addAllowedIPs = void 0;
var Array = _interopRequireWildcard(require("effect/Array"));
var Effect = _interopRequireWildcard(require("effect/Effect"));
var Function = _interopRequireWildcard(require("effect/Function"));
var Match = _interopRequireWildcard(require("effect/Match"));
var Option = _interopRequireWildcard(require("effect/Option"));
var Predicate = _interopRequireWildcard(require("effect/Predicate"));
var Record = _interopRequireWildcard(require("effect/Record"));
var Schema = _interopRequireWildcard(require("effect/Schema"));
var Tuple = _interopRequireWildcard(require("effect/Tuple"));
var assert = _interopRequireWildcard(require("node:assert"));
var InternetSchemas = _interopRequireWildcard(require("./InternetSchemas.js"));
var WireguardConfig = _interopRequireWildcard(require("./WireguardConfig.js"));
var WireguardErrors = _interopRequireWildcard(require("./WireguardErrors.js"));
var WireguardKey = _interopRequireWildcard(require("./WireguardKey.js"));
function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && {}.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; }
/**
 * Tools to help generate wireguard configs for common situations.
 *
 * @since 1.0.0
 */

/** @internal */
const ipForNode = exports.ipForNode = /*#__PURE__*/Function.pipe(/*#__PURE__*/Match.type(), /*#__PURE__*/Match.when(/*#__PURE__*/Schema.is(InternetSchemas.IPv4), ({
  ip
}) => ip), /*#__PURE__*/Match.when(/*#__PURE__*/Schema.is(InternetSchemas.IPv6), ({
  ip
}) => ip), /*#__PURE__*/Match.whenOr(/*#__PURE__*/Schema.is(InternetSchemas.IPv4SetupData), /*#__PURE__*/Schema.is(InternetSchemas.IPv6SetupData), /*#__PURE__*/Schema.is(InternetSchemas.HostnameIPv4SetupData), /*#__PURE__*/Schema.is(InternetSchemas.HostnameIPv6SetupData), ([_, {
  ip
}]) => ip), Match.exhaustive);
/** @internal */
const nodesByIp = nodes => Function.pipe(nodes, Array.map(node => Tuple.make(ipForNode(node), node)), Record.fromEntries);
/** @internal */
exports.nodesByIp = nodesByIp;
const makeEmptyContext = nodes => Record.map(nodesByIp(nodes), Function.constUndefined);
/**
 * Generates private+public keys for all nodes in the network.
 *
 * @since 1.0.0
 * @category Key Transformers
 */
exports.makeEmptyContext = makeEmptyContext;
const generateKeys = nodesLayer => ({
  nodes: nodesLayer.nodes,
  wireguardNetworkCidr: nodesLayer.wireguardNetworkCidr,
  keys: Function.pipe(nodesLayer.nodes, makeEmptyContext, Record.map(WireguardKey.generateKeyPair))
});
/**
 * Generates preshare keys for all nodes in the network.
 *
 * @since 1.0.0
 * @category Key Transformers
 */
exports.generateKeys = generateKeys;
const addPreshareKeys = keysLayer => ({
  nodes: keysLayer.nodes,
  wireguardNetworkCidr: keysLayer.wireguardNetworkCidr,
  keys: Record.map(keysLayer.keys, (keys, node) => node === ipForNode(keysLayer.nodes[0]) ? keys : {
    ...keys,
    preshareKey: WireguardKey.generatePreshareKey()
  })
});
/**
 * Generates connections in a star pattern for all nodes in the network.
 *
 * @since 1.0.0
 * @category Connection Transformers
 */
exports.addPreshareKeys = addPreshareKeys;
const generateStarConnections = keysLayer => {
  const allNodeIps = Array.map(keysLayer.nodes, ipForNode);
  const connections = Function.pipe(keysLayer.nodes, makeEmptyContext, Record.map((_, currentNode) => {
    const neighbors = Array.filter(allNodeIps, ip => ip !== currentNode);
    assert.ok(Array.isNonEmptyReadonlyArray(neighbors), "There must be at least one neighbor");
    return neighbors;
  }));
  return {
    connections,
    nodes: keysLayer.nodes,
    keys: keysLayer.keys,
    wireguardNetworkCidr: keysLayer.wireguardNetworkCidr
  };
};
/**
 * Generates connections in a hub and spoke pattern for all nodes in the
 * network.
 *
 * @since 1.0.0
 * @category Connection Transformers
 */
exports.generateStarConnections = generateStarConnections;
const generateHubAndSpokeConnections = keysLayer => ({
  nodes: keysLayer.nodes,
  keys: keysLayer.keys,
  wireguardNetworkCidr: keysLayer.wireguardNetworkCidr,
  connections: Function.pipe(keysLayer.nodes, makeEmptyContext, Record.map((_, node) => {
    if (node === ipForNode(keysLayer.nodes[0])) {
      return InternetSchemas.tail(keysLayer.nodes);
    } else {
      return Array.of(keysLayer.nodes[0]);
    }
  }), Record.map(Array.map(ipForNode)))
});
/**
 * Adds a direct connection between two nodes in the network.
 *
 * @since 1.0.0
 * @category Connection Transformers
 */
exports.generateHubAndSpokeConnections = generateHubAndSpokeConnections;
const addConnection = exports.addConnection = /*#__PURE__*/Function.dual(3, (connectionsLayer, from, to) => {
  const oldConnections = Record.get(connectionsLayer.connections, from);
  const newConnections = Function.pipe(oldConnections, Option.map(connections => Array.append(connections, to)), Option.getOrElse(() => Array.of(to)));
  return {
    nodes: connectionsLayer.nodes,
    keys: connectionsLayer.keys,
    wireguardNetworkCidr: connectionsLayer.wireguardNetworkCidr,
    connections: Record.set(connectionsLayer.connections, from, newConnections)
  };
});
/**
 * Generates the allowed IPs for each node in the network based on the
 * connections is has to the other nodes.
 *
 * @since 1.0.0
 * @category AllowedIPs Transformers
 */
const computeAllowedIPsFromConnections = connectionsLayer => ({
  nodes: connectionsLayer.nodes,
  keys: connectionsLayer.keys,
  connections: connectionsLayer.connections,
  wireguardNetworkCidr: connectionsLayer.wireguardNetworkCidr,
  allowedIPs: Record.map(connectionsLayer.connections, Array.map(ip => ({
    from: ip,
    block: `${ip}/32`
  })))
});
/**
 * Adds an allowed IP to a node in the network.
 *
 * @since 1.0.0
 * @category AllowedIPs Transformers
 */
exports.computeAllowedIPsFromConnections = computeAllowedIPsFromConnections;
const addAllowedIPs = exports.addAllowedIPs = /*#__PURE__*/Function.dual(4, (allowedIPsLayer, nodeToIp, nodeFromIp, cidrs) => {
  const oldCidrs = Record.get(allowedIPsLayer.allowedIPs, nodeToIp);
  const cidrsEncoded = Array.map(cidrs, cidr => Function.pipe(Match.value(cidr), Match.when(Schema.is(InternetSchemas.IPv4CidrBlock), ({
    address,
    mask
  }) => `${address.ip}/${mask}`), Match.when(Schema.is(InternetSchemas.IPv6CidrBlock), ({
    address,
    mask
  }) => `${address.ip}/${mask}`), Match.when(Predicate.isString, Function.identity), Match.exhaustive));
  const mappedCidrs = Array.map(cidrsEncoded, cidr => ({
    from: nodeFromIp,
    block: cidr
  }));
  const newCidrs = Function.pipe(oldCidrs, Option.map(oldCidrs => Array.appendAll(oldCidrs, mappedCidrs)), Option.getOrElse(() => mappedCidrs));
  return {
    nodes: allowedIPsLayer.nodes,
    keys: allowedIPsLayer.keys,
    connections: allowedIPsLayer.connections,
    wireguardNetworkCidr: allowedIPsLayer.wireguardNetworkCidr,
    allowedIPs: Record.set(allowedIPsLayer.allowedIPs, nodeToIp, newCidrs)
  };
});
/**
 * Converts a network into configs.
 *
 * @since 1.0.0
 * @category Generator
 */
const toConfigs = ({
  allowedIPs,
  connections,
  keys,
  nodes,
  wireguardNetworkCidr
}) => {
  const endpointsByIp = nodesByIp(nodes);
  const peerConfigsByIp = Function.pipe(nodes, nodesByIp, Record.map(node => Effect.gen(function* () {
    const keysForNode = Record.get(keys, ipForNode(node)).pipe(Option.getOrThrow);
    const endpointForNode = Record.get(endpointsByIp, ipForNode(node)).pipe(Option.getOrThrow);
    const endpointForNodeEncoded = yield* Schema.is(InternetSchemas.SetupData)(endpointForNode) ? Schema.encode(InternetSchemas.SetupData)(endpointForNode).pipe(Effect.map(x => x[0])) : Effect.succeed(undefined);
    const address = Schema.is(InternetSchemas.SetupData)(endpointForNode) ? endpointForNode[1].ip : endpointForNode.ip;
    return {
      Address: address,
      Endpoint: endpointForNodeEncoded,
      PublicKey: keysForNode.publicKey,
      PresharedKey: keysForNode.preshareKey
    };
  })));
  const configs = Array.map(nodes, node => Effect.gen(function* () {
    const keysForNode = Record.get(keys, ipForNode(node)).pipe(Option.getOrThrow);
    const endpointForNode = Record.get(endpointsByIp, ipForNode(node)).pipe(Option.getOrThrow);
    const allowedIPsForNode = Record.get(allowedIPs, ipForNode(node)).pipe(Option.getOrThrow);
    const address = Schema.is(InternetSchemas.SetupData)(endpointForNode) ? endpointForNode[1].ip : endpointForNode.ip;
    const listenPort = Schema.is(InternetSchemas.SetupData)(endpointForNode) ? endpointForNode[0].listenPort : 0;
    const directConnectionsForNode = yield* Function.pipe(connections, Record.get(ipForNode(node)), Option.getOrThrow, Array.filterMap(peerAddress => Record.get(peerConfigsByIp, peerAddress)), Effect.allWith());
    const dcsWithAllowedIPs = Array.map(directConnectionsForNode, dc => ({
      Endpoint: dc.Endpoint,
      PublicKey: dc.PublicKey,
      PresharedKey: node === nodes[0] ? dc.PresharedKey : keysForNode.preshareKey,
      AllowedIPs: Function.pipe(allowedIPsForNode, Array.filter(({
        from
      }) => from === dc.Address), Array.map(({
        block
      }) => block), x => new Set(x))
    }));
    return yield* Schema.decode(WireguardConfig.WireguardConfig)({
      ListenPort: listenPort,
      Peers: dcsWithAllowedIPs,
      PrivateKey: keysForNode.privateKey,
      Address: `${address}/${wireguardNetworkCidr.mask}`
    });
  }));
  if (Predicate.isTupleOfAtLeast(configs, 2)) {
    return Effect.all(configs);
  } else {
    return Effect.fail(new WireguardErrors.WireguardError({
      message: "There must be at least two nodes"
    }));
  }
};
/**
 * Use your phone or computer to remotely access just the wireguard server.
 *
 * @since 1.0.0
 * @category Generators
 */
exports.toConfigs = toConfigs;
const generateRemoteAccessToServer = options => Function.flow(generateKeys, addPreshareKeys, generateStarConnections, computeAllowedIPsFromConnections)(options);
/**
 * Builds on "Remote access to server", allowing you to access your entire LAN
 * as well.
 *
 * @since 1.0.0
 * @category Generators
 */
exports.generateRemoteAccessToServer = generateRemoteAccessToServer;
const generateRemoteAccessToLan = options => Function.flow(generateRemoteAccessToServer, addAllowedIPs(ipForNode(options.nodes[1]), ipForNode(options.nodes[0]), Array.isArray(options.lanNetworkCidr) ? options.lanNetworkCidr : Array.of(options.lanNetworkCidr)))(options);
/**
 * Allows two servers to connect to each other.
 *
 * @since 1.0.0
 * @category Generators
 */
exports.generateRemoteAccessToLan = generateRemoteAccessToLan;
const generateServerToServerAccess = options => Function.flow(generateKeys, addPreshareKeys, generateHubAndSpokeConnections, computeAllowedIPsFromConnections)(options);
/**
 * Builds on "Server to server access", allowing two entire networks to
 * communicate.
 *
 * @since 1.0.0
 * @category Generators
 */
exports.generateServerToServerAccess = generateServerToServerAccess;
const generateLanToLanAccess = options => Function.flow(generateServerToServerAccess, addAllowedIPs(ipForNode(options.nodes[1]), ipForNode(options.nodes[0]), Array.isArray(options.server1Lan) ? options.server1Lan : Array.of(options.server1Lan)), addAllowedIPs(ipForNode(options.nodes[0]), ipForNode(options.nodes[1]), Array.isArray(options.server2Lan) ? options.server2Lan : Array.of(options.server2Lan)))(options);
/**
 * Builds on "Remote access to server", except that all of the VPN clients can
 * connect to each other as well. Note: all traffic between nodes must pass
 * through the server.
 *
 * @since 1.0.0
 * @category Generators
 */
exports.generateLanToLanAccess = generateLanToLanAccess;
const generateServerHubAndSpokeAccess = options => {
  const clientNodes = Function.pipe(options.nodes, Array.map(ipForNode), Array.tailNonEmpty);
  const firstFoldOnClientNodes = Array.reduce(clientNodes, Function.identity, (outerAcc, node) => {
    const secondFoldOverOtherNodes = Array.reduce(Array.filter(clientNodes, n => n !== node), Function.identity, (innerAcc, otherNode) => Function.compose(addAllowedIPs(node, ipForNode(options.nodes[0]), [`${otherNode}/32`]), innerAcc));
    return Function.compose(secondFoldOverOtherNodes, outerAcc);
  });
  const networkWithoutSpokeAccess = Function.flow(generateKeys, addPreshareKeys, generateHubAndSpokeConnections, computeAllowedIPsFromConnections)(options);
  return firstFoldOnClientNodes(networkWithoutSpokeAccess);
};
/**
 * Builds on "Server hub and spoke access", allowing you to access your entire
 * LAN as well.
 *
 * @since 1.0.0
 * @category Generators
 */
exports.generateServerHubAndSpokeAccess = generateServerHubAndSpokeAccess;
const generateLanHubAndSpokeAccess = options => {
  const addAllowedIPsToAllNodes = Function.pipe(options.nodes, Array.map(ipForNode), Array.tailNonEmpty, Array.reduce(Function.identity, (acc, node) => Function.compose(addAllowedIPs(node, ipForNode(options.nodes[0]), Array.isArray(options.lanNetworkCidr) ? options.lanNetworkCidr : Array.of(options.lanNetworkCidr)), acc)));
  return addAllowedIPsToAllNodes(generateServerHubAndSpokeAccess(options));
};
/**
 * Route specific traffic through a commercial WireGuard VPN provider.
 *
 * @since 1.0.0
 * @category Generators
 */
exports.generateLanHubAndSpokeAccess = generateLanHubAndSpokeAccess;
const generateVpnTunneledAccess = options => Function.flow(generateRemoteAccessToServer, addAllowedIPs(ipForNode(options.nodes[1]), ipForNode(options.nodes[0]), ["0.0.0.0/0"]))(options);
/**
 * Securely access the Internet from untrusted networks by routing all of your
 * traffic through the VPN and out the server's internet connection.
 *
 * @since 1.0.0
 * @category Generators
 */
exports.generateVpnTunneledAccess = generateVpnTunneledAccess;
const generateRemoteTunneledAccess = options => Function.flow(generateRemoteAccessToLan, addAllowedIPs(ipForNode(options.nodes[1]), ipForNode(options.nodes[0]), ["0.0.0.0/0"]))(options);
exports.generateRemoteTunneledAccess = generateRemoteTunneledAccess;
//# sourceMappingURL=WireguardGenerate.js.map